Mizan's protocol layers (origin Redis cache, Edge Worker) handle caching
autonomously. The origin emits Cache-Control: no-store on ALL responses —
browsers and non-Mizan intermediaries must not cache. The Edge Worker
controls CDN caching via cf object, independent of origin headers.
Also fixes:
- TS localeCompare → byte-order sort (localeCompare is locale-sensitive,
would produce different HMAC keys for non-ASCII params vs Python)
- Python cache_purge: empty {} params no longer treated as falsy
(was inconsistent with JS where {} is truthy)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
/**
|
|
* Cache key derivation — HMAC-SHA256 over JSON-canonical form.
|
|
*
|
|
* Protocol-critical: must produce identical output to Python's derive_cache_key.
|
|
* Cross-language conformance verified by pin tests.
|
|
*
|
|
* Key format: "ctx:{context}:{hmac_hex}" — enables broad purge by prefix scan.
|
|
*/
|
|
|
|
import { createHmac } from 'crypto'
|
|
|
|
const CONTEXT_KEY_PREFIX = 'ctx:'
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* Returns "ctx:{context}:{hmac_hex}".
|
|
*/
|
|
export function deriveCacheKey(
|
|
secret: string,
|
|
context: string,
|
|
params: Record<string, any>,
|
|
userId?: string,
|
|
rev: number = 0,
|
|
): string {
|
|
const sortedParams: Record<string, string> = {}
|
|
for (const [k, v] of Object.entries(params).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
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)
|
|
const hmacHex = createHmac('sha256', secret).update(message).digest('hex')
|
|
return `${CONTEXT_KEY_PREFIX}${context}:${hmacHex}`
|
|
}
|
|
|
|
export { CONTEXT_KEY_PREFIX }
|