import type { CacheScopeSnapshot } from "../../contracts/server-state-scope.ts"; export type ConditionalValidatorBinding = Readonly<{ definitionId: string; identityToken: string; representationVersion: number; scope: CacheScopeSnapshot; }>; export type ConditionalValidatorStore = Readonly<{ install( binding: ConditionalValidatorBinding, validator: string, cacheRevision: number, ): boolean; prepare( binding: ConditionalValidatorBinding, cacheRevision: number, ): string | null; acceptNotModified( binding: ConditionalValidatorBinding, cacheRevision: number, hasMappedValue: boolean, ): boolean; remove(binding: ConditionalValidatorBinding): void; clear(): void; }>; type ValidatorRow = { validator: string; cacheRevision: number; generation: number; }; type ConditionalValidatorKeyTuple = readonly [ scopeFingerprint: string, definitionId: string, identityToken: string, representationVersion: number, ]; /** The store is a trust boundary, so key components are validated and bounded. */ const MAX_KEY_COMPONENT_BYTES = 512; const KEY_COMPONENT_ENCODER = new TextEncoder(); function isBoundedKeyComponent(value: unknown): value is string { return ( typeof value === "string" && value.length > 0 && KEY_COMPONENT_ENCODER.encode(value).byteLength <= MAX_KEY_COMPONENT_BYTES ); } export function createConditionalValidatorStore( maxEntries = 1_024, ): ConditionalValidatorStore { if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) { throw new TypeError("Invalid conditional validator capacity."); } const rows = new Map(); /** * N-05. A delimiter join is not injective here: `definitionId`, * `identityToken` and the scope fingerprint may all contain the delimiter, so * two distinct valid bindings could encode to the same key and one * definition's ETag could be sent for another. The key is a validated fixed * tuple encoded with `JSON.stringify`, which escapes the separators. */ function key(binding: ConditionalValidatorBinding): string | null { if ( !binding.scope.isCurrent() || !isBoundedKeyComponent(binding.scope.fingerprint) || !isBoundedKeyComponent(binding.definitionId) || !/^[A-Za-z0-9._:-]{16,128}$/.test(binding.identityToken) || !Number.isSafeInteger(binding.representationVersion) || binding.representationVersion < 1 ) { return null; } const tuple: ConditionalValidatorKeyTuple = [ binding.scope.fingerprint, binding.definitionId, binding.identityToken, binding.representationVersion, ]; return JSON.stringify(tuple); } return Object.freeze({ install(binding, validator, cacheRevision) { const selectedKey = key(binding); if ( !selectedKey || !isSafeEntityTag(validator) || !Number.isSafeInteger(cacheRevision) || cacheRevision < 0 ) { return false; } if (!rows.has(selectedKey) && rows.size >= maxEntries) return false; rows.set(selectedKey, { validator, cacheRevision, generation: binding.scope.generation, }); return true; }, prepare(binding, cacheRevision) { const selectedKey = key(binding); if (!selectedKey) return null; const row = rows.get(selectedKey); return row && row.generation === binding.scope.generation && row.cacheRevision === cacheRevision ? row.validator : null; }, acceptNotModified(binding, cacheRevision, hasMappedValue) { const selectedKey = key(binding); if (!selectedKey || !hasMappedValue) return false; const row = rows.get(selectedKey); return Boolean( row && row.generation === binding.scope.generation && row.cacheRevision === cacheRevision, ); }, remove(binding) { const selectedKey = key(binding); if (selectedKey) rows.delete(selectedKey); }, clear() { rows.clear(); }, }); } function isSafeEntityTag(value: string): boolean { if (value.length < 3 || value.length > 256) return false; const opaque = value.startsWith('W/"') ? value.slice(3, -1) : value.startsWith('"') ? value.slice(1, -1) : null; if (opaque === null || !value.endsWith('"')) return false; return [...opaque].every((character) => { const code = character.codePointAt(0) ?? 0; return code === 0x21 || (code >= 0x23 && code <= 0x7e) || (code >= 0x80 && code <= 0xff); }); }