Add TypeScript cache adapter with cross-language conformance tests
Port of Python's origin-side cache to TypeScript: - cache/keys.ts: deriveCacheKey with stableStringify for JSON-canonical HMAC - cache/backend.ts: MemoryCache (same API as Python) - cache/index.ts: cacheGet, cachePut, cachePurge with AND semantics Integrated into dispatch.ts: - handleContextFetch: cache lookup before execution, store after - handleMutationCall: purge on invalidation Cross-language pin test proves Python and TypeScript produce identical HMAC-SHA256 output for the same inputs: Public: 605a1ca5ad5994e9b765c8d1b330474c2a0d51a7b8fbbdc402f992da7ba902f6 User-scoped: 30fc08eb46ee4ff2cf7d317e97dca90fd616511e0587304416f71dc863338dc2 34 TypeScript tests (9 new), 165 Python tests (1 new pin test). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
69
packages/mizan-ts/src/cache/keys.ts
vendored
Normal file
69
packages/mizan-ts/src/cache/keys.ts
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Cache key derivation — HMAC-SHA256 over JSON-canonical form.
|
||||
*
|
||||
* Protocol-critical: must produce identical output to Python's derive_cache_key
|
||||
* for the same inputs. Cross-language conformance verified by pin tests.
|
||||
*
|
||||
* Wire format:
|
||||
* HMAC-SHA256(secret, stableStringify({"c": ctx, "p": params, "r": rev}))
|
||||
* - All keys sorted recursively
|
||||
* - No whitespace (compact JSON)
|
||||
* - "u" key included only for user-scoped content
|
||||
* - All param values stringified
|
||||
*/
|
||||
|
||||
import { createHmac } from 'crypto'
|
||||
|
||||
/**
|
||||
* JSON.stringify with recursively sorted keys and no whitespace.
|
||||
* Equivalent to Python's json.dumps(obj, sort_keys=True, separators=(",", ":"))
|
||||
*/
|
||||
function stableStringify(obj: any): string {
|
||||
if (obj === null || obj === undefined) return 'null'
|
||||
if (typeof obj === 'string') return JSON.stringify(obj)
|
||||
if (typeof obj === 'number' || typeof obj === 'boolean') return String(obj)
|
||||
if (Array.isArray(obj)) {
|
||||
return '[' + obj.map(stableStringify).join(',') + ']'
|
||||
}
|
||||
const keys = Object.keys(obj).sort()
|
||||
const pairs = keys.map(k => JSON.stringify(k) + ':' + stableStringify(obj[k]))
|
||||
return '{' + pairs.join(',') + '}'
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a deterministic HMAC-SHA256 cache key.
|
||||
*
|
||||
* Must produce identical output to Python's derive_cache_key for the same inputs.
|
||||
*/
|
||||
export function deriveCacheKey(
|
||||
secret: string,
|
||||
context: string,
|
||||
params: Record<string, any>,
|
||||
userId?: string,
|
||||
rev: number = 0,
|
||||
): string {
|
||||
// Stringify all param values for cross-language determinism
|
||||
const sortedParams: Record<string, string> = {}
|
||||
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
sortedParams[k] = String(v)
|
||||
}
|
||||
|
||||
const keyData: Record<string, any> = { c: context, p: sortedParams, r: rev }
|
||||
if (userId !== undefined) {
|
||||
keyData.u = String(userId)
|
||||
}
|
||||
|
||||
const message = stableStringify(keyData)
|
||||
return createHmac('sha256', secret).update(message).digest('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build reverse index keys for a cache entry.
|
||||
*/
|
||||
export function buildIndexKeys(context: string, params: Record<string, any>): string[] {
|
||||
const keys = [`mizan:idx:${context}`]
|
||||
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a.localeCompare(b))) {
|
||||
keys.push(`mizan:idx:${context}:${k}=${String(v)}`)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
Reference in New Issue
Block a user