import { describe, expect, it } from "vitest"; import type { PublicCacheAsset, PublicCacheReleaseManifest, } from "../../src/application/ports/browser-file-storage/cache-storage-ports.ts"; import { createDefaultPublicCachePolicy, resolvePublicCachePolicy, type PublicCacheRuntimePolicy, } from "../../src/adapters/cache-storage/public-cache-policy.ts"; import { computePublicCacheManifestDigestHex, createPublicResponseCacheAdapter, type PublicCacheMutationLock, } from "../../src/adapters/cache-storage/public-response-cache-adapter.ts"; class MemoryCache { readonly responses: Array> = []; async match(request: RequestInfo | URL): Promise { const url = request instanceof Request ? request.url : new URL(String(request)).href; const nativeRequest = request instanceof Request ? request : new Request(url); return this.responses .find( (entry) => entry.request.url === url && varyMatches(entry.request, nativeRequest, entry.response), ) ?.response.clone(); } async put(request: RequestInfo | URL, response: Response): Promise { const url = request instanceof Request ? request.url : new URL(String(request)).href; const nativeRequest = request instanceof Request ? request.clone() : new Request(url); const existing = this.responses.findIndex( (entry) => entry.request.url === url && varyMatches(entry.request, nativeRequest, response), ); const entry = { request: nativeRequest, response: response.clone(), }; if (existing >= 0) this.responses.splice(existing, 1, entry); else this.responses.push(entry); } } function varyMatches( storedRequest: Request, incomingRequest: Request, response: Response, ): boolean { const vary = response.headers.get("vary"); if (!vary) return true; return vary .split(",") .map((name) => name.trim().toLowerCase()) .every( (name) => storedRequest.headers.get(name) === incomingRequest.headers.get(name), ); } class MemoryCacheStorage { readonly caches = new Map(); async open(name: string): Promise { let cache = this.caches.get(name); if (!cache) { cache = new MemoryCache(); this.caches.set(name, cache); } return cache as unknown as Cache; } async keys(): Promise { return [...this.caches.keys()]; } async delete(name: string): Promise { return this.caches.delete(name); } } const immediateLock: PublicCacheMutationLock = { async run(_signal, task) { return await task(); }, }; function deferred() { let settle: ((value: Value) => void) | undefined; const promise = new Promise((resolve) => { settle = resolve; }); return Object.freeze({ promise, resolve(value: Value): void { settle?.(value); }, }); } async function digestHex(bytes: Uint8Array): Promise { const digest = await globalThis.crypto.subtle.digest( "SHA-256", Uint8Array.from(bytes), ); return [...new Uint8Array(digest)] .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); } async function manifestFor( releaseRegistryId: string, assets: readonly PublicCacheAsset[], policy: PublicCacheRuntimePolicy = createDefaultPublicCachePolicy( "https://assets.example.test", ), ): Promise { return { releaseRegistryId, assets, manifestDigestHex: await computePublicCacheManifestDigestHex( globalThis.crypto, releaseRegistryId, assets, policy, ), }; } describe("public response Cache Storage adapter", () => { it("rejects a policy that enables variants but strips Vary", () => { const base = createDefaultPublicCachePolicy( "https://assets.example.test", ); // STO-03. Enabling variants while dropping Vary from the response // allowlist makes every stored variant collide on the same key. expect(() => resolvePublicCachePolicy({ ...base, allowedRequestHeaderNames: ["accept", "accept-language"], allowedVaryHeaderNames: ["accept-language"], allowedResponseHeaderNames: base.allowedResponseHeaderNames.filter( (name) => name !== "vary", ), }), ).toThrow(TypeError); // The same policy with Vary preserved is accepted. expect(() => resolvePublicCachePolicy({ ...base, allowedRequestHeaderNames: ["accept", "accept-language"], allowedVaryHeaderNames: ["accept-language"], }), ).not.toThrow(); }); it("restages an evicted entry even when the release marker remains", async () => { const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([9, 9, 9, 9]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/evicted.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const cacheStorage = new MemoryCacheStorage(); let fetches = 0; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => { fetches += 1; return new Response(bytes, { headers: { "cache-control": "public", "content-type": "application/javascript", }, }); }, }); const manifest = await manifestFor("evicted-release", [asset], policy); expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); expect(fetches).toBe(1); // The browser evicts the payload but leaves the marker behind. const cacheName = [...cacheStorage.caches.keys()].find((name) => name.includes("evicted-release"), ); if (!cacheName) throw new Error("staged cache missing"); const cache = cacheStorage.caches.get(cacheName)!; const payloadIndex = cache.responses.findIndex( (entry) => entry.request.url === asset.absoluteUrl, ); expect(payloadIndex).toBeGreaterThanOrEqual(0); cache.responses.splice(payloadIndex, 1); // A marker is a claim, not evidence: restaging must repair. expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); expect(fetches).toBe(2); expect( await adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); }); it("activates a verified prestaged release without a fetcher", async () => { const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([4, 4, 4, 4]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/offline.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const cacheStorage = new MemoryCacheStorage(); const online = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public", "content-type": "application/javascript", }, }), }); const manifest = await manifestFor("offline-release", [asset], policy); expect(await online.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); // STO-05. Activation and cleanup perform no network I/O, so a missing // fetcher must not make an offline rollback UNSUPPORTED. const offline = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, }); expect( await offline.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); }); it("cleans exact owned caches without a fetcher", async () => { const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const cacheStorage = new MemoryCacheStorage(); const offline = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, }); expect(await offline.admin.cleanupOwned()).toMatchObject({ ok: true, }); }); it("stages, re-verifies and atomically activates an exact release", async () => { const cacheStorage = new MemoryCacheStorage(); const bytes = new Uint8Array([1, 2, 3, 4]); const url = "https://assets.example.test/app.js?v=release-1"; const asset: PublicCacheAsset = { absoluteUrl: url, expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const policy: PublicCacheRuntimePolicy = { ...createDefaultPublicCachePolicy( "https://assets.example.test", ), allowedQueryParameterNames: ["v"], isQueryParameterValueAllowed: (name, value) => name === "v" && /^release-\d+$/u.test(value), }; const manifest = await manifestFor("release-1", [asset], policy); const observations: unknown[] = []; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, now: () => 10, observer: (event) => observations.push(event), fetcher: async () => new Response(bytes, { status: 200, headers: { "cache-control": "public, max-age=31536000, immutable", "content-type": "application/javascript", "x-user-id": "must-not-be-persisted", }, }), }); const staged = await adapter.admin.stageRelease(manifest); expect(staged).toEqual({ ok: true, value: { releaseRegistryId: "release-1", entryCount: 1, totalBytes: 4, stagedAtEpochMs: 10, }, }); expect( await adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); const match = await adapter.responses.matchActiveExact({ absoluteUrl: url, }); expect(match.ok).toBe(true); if (!match.ok || !match.value) throw new Error("cache miss"); const chunks: number[] = []; for await (const chunk of match.value.body.stream( new AbortController().signal, )) { expect(chunk.ok).toBe(true); if (chunk.ok) chunks.push(...chunk.value); } expect(chunks).toEqual([1, 2, 3, 4]); expect(match.value.headers.map(([name]) => name)).not.toContain( "x-user-id", ); expect( await adapter.responses.matchActiveExact({ absoluteUrl: "https://assets.example.test/app.js?v=release-2", }), ).toEqual({ ok: true, value: null }); expect(JSON.stringify(observations)).not.toContain(url); expect(JSON.stringify(observations)).not.toContain( manifest.manifestDigestHex, ); }); it("snapshots the complete manifest before asynchronous digest verification", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([2, 4, 6, 8]); const asset = { absoluteUrl: "https://assets.example.test/snapshot-crypto.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256" as const, digestHex: await digestHex(bytes), }, }; const signed = await manifestFor( "snapshot-crypto", [asset], policy, ); const manifest = { releaseRegistryId: signed.releaseRegistryId, assets: [asset], manifestDigestHex: signed.manifestDigestHex, }; const digestStarted = deferred(); const releaseDigest = deferred(); let delayFirstDigest = true; const realCrypto = globalThis.crypto; const delayedCrypto = { subtle: { async digest( algorithm: AlgorithmIdentifier, data: BufferSource, ): Promise { if (delayFirstDigest) { delayFirstDigest = false; digestStarted.resolve(undefined); await releaseDigest.promise; } return await realCrypto.subtle.digest(algorithm, data); }, }, } as unknown as Crypto; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: delayedCrypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); const staging = adapter.admin.stageRelease(manifest); await digestStarted.promise; manifest.releaseRegistryId = "mutated-during-crypto"; manifest.manifestDigestHex = "0".repeat(64); manifest.assets[0]!.absoluteUrl = "https://assets.example.test/mutated-crypto.js"; releaseDigest.resolve(undefined); await expect(staging).resolves.toMatchObject({ ok: true, value: { releaseRegistryId: "snapshot-crypto" }, }); await expect( adapter.admin.activateRelease( signed.releaseRegistryId, signed.manifestDigestHex, ), ).resolves.toMatchObject({ ok: true }); }); it("keeps the original stage signal while digest verification is pending", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([8, 6, 4, 2]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/snapshot-signal.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor( "snapshot-signal", [asset], policy, ); const digestStarted = deferred(); const releaseDigest = deferred(); let delayFirstDigest = true; const realCrypto = globalThis.crypto; const delayedCrypto = { subtle: { async digest( algorithm: AlgorithmIdentifier, data: BufferSource, ): Promise { if (delayFirstDigest) { delayFirstDigest = false; digestStarted.resolve(undefined); await releaseDigest.promise; } return await realCrypto.subtle.digest(algorithm, data); }, }, } as unknown as Crypto; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: delayedCrypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); const original = new AbortController(); const replacement = new AbortController(); const options = { signal: original.signal }; const staging = adapter.admin.stageRelease(manifest, options); await digestStarted.promise; options.signal = replacement.signal; original.abort(); releaseDigest.resolve(undefined); await expect(staging).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); await expect(cacheStorage.keys()).resolves.toEqual([]); }); /** * NS-08. Handing the signal to each `Request` only asked a cooperative fetch * to stop. A stream that ignored it held the mutation lock forever, and work * that finished after the abort still wrote its asset and its marker. */ it("does not wait for a non-cooperative fetch after the caller aborts", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([1, 2, 3, 4]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/never-settles.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) }, }; const manifest = await manifestFor("never-settles", [asset], policy); const fetchStarted = deferred(); let locksHeld = 0; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: { async run(signal, operation) { locksHeld += 1; try { return await operation(); } finally { locksHeld -= 1; } }, }, policy, fetcher: () => { fetchStarted.resolve(undefined); // Ignores the signal entirely. return new Promise(() => {}); }, }); const controller = new AbortController(); const staging = adapter.admin.stageRelease(manifest, { signal: controller.signal, }); await fetchStarted.promise; controller.abort(); await expect(staging).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); expect(locksHeld).toBe(0); await expect(cacheStorage.keys()).resolves.toEqual([]); }); it("writes neither asset nor marker when a digest completes after the abort", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([9, 9, 9, 9]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/late-digest.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes) }, }; const manifest = await manifestFor("late-digest", [asset], policy); const digestStarted = deferred(); const releaseDigest = deferred(); let delayFirstDigest = true; const realCrypto = globalThis.crypto; const delayedCrypto = { subtle: { async digest( algorithm: AlgorithmIdentifier, data: BufferSource, ): Promise { if (delayFirstDigest) { delayFirstDigest = false; digestStarted.resolve(undefined); await releaseDigest.promise; } return await realCrypto.subtle.digest(algorithm, data); }, }, } as unknown as Crypto; let puts = 0; const adapter = createPublicResponseCacheAdapter({ cacheStorage: new Proxy(cacheStorage, { get(target, key, receiver) { if (key === "open") { return async (name: string) => { const cache = await target.open(name); return new Proxy(cache, { get(cacheTarget, cacheKey, cacheReceiver) { if (cacheKey === "put") { return async (...args: readonly unknown[]) => { puts += 1; return await ( cacheTarget.put as ( ...values: readonly unknown[] ) => Promise )(...args); }; } return Reflect.get(cacheTarget, cacheKey, cacheReceiver); }, }); }; } return Reflect.get(target, key, receiver); }, }) as unknown as CacheStorage, crypto: delayedCrypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); const controller = new AbortController(); const staging = adapter.admin.stageRelease(manifest, { signal: controller.signal, }); await digestStarted.promise; controller.abort(); releaseDigest.resolve(undefined); await expect(staging).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); // Neither the asset nor the activation marker may be written by work the // abort already disowned. expect(puts).toBe(0); }); it("keeps the original activation signal while waiting for the mutation lock", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([7]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/activate-signal.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor("activate-signal", [asset], policy); const lockStarted = deferred(); const releaseLock = deferred(); let delayLock = false; const lock: PublicCacheMutationLock = { async run(_signal, task) { if (delayLock) { lockStarted.resolve(undefined); await releaseLock.promise; } return await task(); }, }; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: lock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({ ok: true, }); delayLock = true; const original = new AbortController(); const replacement = new AbortController(); const options = { signal: original.signal }; const activation = adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, options, ); await lockStarted.promise; options.signal = replacement.signal; original.abort(); releaseLock.resolve(undefined); await expect(activation).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); expect(await cacheStorage.keys()).not.toContain("ca-public-v1:control"); }); it("keeps the original lookup signal while cache discovery is pending", async () => { const cacheStorage = new MemoryCacheStorage(); const baseKeys = cacheStorage.keys.bind(cacheStorage); const lookupStarted = deferred(); const releaseLookup = deferred(); let delayKeys = false; cacheStorage.keys = async () => { if (delayKeys) { lookupStarted.resolve(undefined); await releaseLookup.promise; delayKeys = false; } return await baseKeys(); }; const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([9]); const absoluteUrl = "https://assets.example.test/lookup-signal.js"; const asset: PublicCacheAsset = { absoluteUrl, expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor("lookup-signal", [asset], policy); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({ ok: true, }); await expect( adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).resolves.toMatchObject({ ok: true }); delayKeys = true; const original = new AbortController(); const replacement = new AbortController(); const request = { absoluteUrl, signal: original.signal }; const lookup = adapter.responses.matchActiveExact(request); await lookupStarted.promise; request.signal = replacement.signal; original.abort(); releaseLookup.resolve(undefined); await expect(lookup).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); }); it("captures runtime dependencies when the adapter is composed", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([5, 4, 3, 2, 1]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/dependency-snapshot.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor( "dependency-snapshot", [asset], policy, ); const mutableSubtle = { async digest( algorithm: AlgorithmIdentifier, data: BufferSource, ): Promise { return await globalThis.crypto.subtle.digest(algorithm, data); }, }; const mutableCrypto = { subtle: mutableSubtle, } as unknown as Crypto; const mutationLock: PublicCacheMutationLock = { async run(_signal, task) { return await task(); }, }; const dependencies = { cacheStorage: cacheStorage as unknown as CacheStorage, crypto: mutableCrypto, mutationLock, policy, fetcher: async () => new Response(bytes, { status: 200, headers: { "cache-control": "public, max-age=31536000, immutable", "content-type": "application/javascript", }, }), }; const adapter = createPublicResponseCacheAdapter(dependencies); dependencies.fetcher = async () => { throw new TypeError("mutated dependency must not be observed"); }; cacheStorage.keys = async () => { throw new TypeError("mutated CacheStorage method must not be observed"); }; mutationLock.run = async () => { throw new TypeError("mutated lock method must not be observed"); }; mutableSubtle.digest = async () => { throw new TypeError("mutated crypto method must not be observed"); }; await expect( adapter.admin.stageRelease(manifest), ).resolves.toMatchObject({ ok: true, value: { releaseRegistryId: "dependency-snapshot" }, }); }); it("keeps the verified manifest snapshot while waiting for the mutation lock", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([1, 3, 5, 7]); const asset = { absoluteUrl: "https://assets.example.test/snapshot-lock.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256" as const, digestHex: await digestHex(bytes), }, }; const signed = await manifestFor("snapshot-lock", [asset], policy); const manifest = { releaseRegistryId: signed.releaseRegistryId, assets: [asset], manifestDigestHex: signed.manifestDigestHex, }; const lockStarted = deferred(); const releaseLock = deferred(); const delayedLock: PublicCacheMutationLock = { async run(_signal, task) { lockStarted.resolve(undefined); await releaseLock.promise; return await task(); }, }; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: delayedLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); const staging = adapter.admin.stageRelease(manifest); await lockStarted.promise; manifest.releaseRegistryId = "mutated-during-lock"; manifest.manifestDigestHex = "f".repeat(64); manifest.assets[0]!.absoluteUrl = "https://assets.example.test/mutated-lock.js"; releaseLock.resolve(undefined); await expect(staging).resolves.toMatchObject({ ok: true, value: { releaseRegistryId: "snapshot-lock" }, }); await expect( adapter.admin.activateRelease( signed.releaseRegistryId, signed.manifestDigestHex, ), ).resolves.toMatchObject({ ok: true }); }); it("fails closed for private responses and deletes the incomplete candidate", async () => { const cacheStorage = new MemoryCacheStorage(); const bytes = new Uint8Array([9]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/private.json", expectedByteLength: 1, expectedContentType: "application/json", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor("release-private", [asset]); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy: createDefaultPublicCachePolicy( "https://assets.example.test", ), fetcher: async () => new Response(bytes, { headers: { "cache-control": "private, max-age=60", "content-type": "application/json", }, }), }); expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); expect(await cacheStorage.keys()).toEqual([]); }); it("binds the exact expected Content-Type into the signed manifest and cached response", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([7, 7]); const baseAsset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/content.bin", expectedByteLength: bytes.byteLength, expectedContentType: "application/json", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const jsonManifest = await manifestFor( "content-json", [baseAsset], policy, ); const javascriptManifest = await manifestFor( "content-js", [ { ...baseAsset, expectedContentType: "application/javascript", }, ], policy, ); expect(jsonManifest.manifestDigestHex).not.toBe( javascriptManifest.manifestDigestHex, ); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); expect( await adapter.admin.stageRelease(jsonManifest), ).toMatchObject({ ok: false, error: { code: "INTEGRITY_FAILED" }, }); expect(await cacheStorage.keys()).toEqual([]); }); it("deletes only caches owned by its prefix", async () => { const cacheStorage = new MemoryCacheStorage(); await cacheStorage.open("unrelated-cache"); await cacheStorage.open("ca-public-v1:stale"); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy: createDefaultPublicCachePolicy( "https://assets.example.test", ), fetcher: async () => new Response(), }); expect(await adapter.admin.cleanupOwned()).toMatchObject({ ok: true, value: { deletedOwnedCaches: 1 }, }); expect(await cacheStorage.keys()).toContain("unrelated-cache"); }); it("does not let callers retain an unreferenced staged release", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([3, 1, 4]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/orphan.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor("orphan-release", [asset], policy); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }), }); await expect(adapter.admin.stageRelease(manifest)).resolves.toMatchObject({ ok: true, }); const legacyCallerRetention = { retainReleaseRegistryIds: [manifest.releaseRegistryId], } as unknown as Parameters< typeof adapter.admin.cleanupOwned >[0]; await expect( adapter.admin.cleanupOwned(legacyCallerRetention), ).resolves.toMatchObject({ ok: true, value: { deletedOwnedCaches: 1, retainedOwnedCaches: 0, }, }); await expect(cacheStorage.keys()).resolves.toEqual([]); }); it("keeps the original cleanup signal while waiting for the mutation lock", async () => { const cacheStorage = new MemoryCacheStorage(); await cacheStorage.open("ca-public-v1:stale"); const lockStarted = deferred(); const releaseLock = deferred(); const lock: PublicCacheMutationLock = { async run(_signal, task) { lockStarted.resolve(undefined); await releaseLock.promise; return await task(); }, }; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: lock, policy: createDefaultPublicCachePolicy( "https://assets.example.test", ), fetcher: async () => new Response(), }); const original = new AbortController(); const replacement = new AbortController(); const request = { signal: original.signal }; const cleanup = adapter.admin.cleanupOwned(request); await lockStarted.promise; request.signal = replacement.signal; original.abort(); releaseLock.resolve(undefined); await expect(cleanup).resolves.toMatchObject({ ok: false, error: { code: "ABORTED" }, }); await expect(cacheStorage.keys()).resolves.toContain( "ca-public-v1:stale", ); }); it("cancels oversized streamed control JSON before reading the remainder", async () => { let pulls = 0; let cancelled = false; const oversizedControlResponse = new Response( new ReadableStream({ pull(controller) { pulls += 1; if (pulls === 1) { controller.enqueue(new Uint8Array(1_500_000)); return; } if (pulls === 2) { controller.enqueue(new Uint8Array(1_000_000)); return; } controller.enqueue(new Uint8Array([1])); }, cancel() { cancelled = true; }, }), { headers: { "content-type": "application/json" }, }, ); const candidate = { async match(): Promise { return oversizedControlResponse; }, async put(): Promise {}, }; const cacheStorage = { async open(): Promise { return candidate as unknown as Cache; }, async keys(): Promise { return ["ca-public-v1:release:oversized:control"]; }, async delete(): Promise { return false; }, }; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, policy: createDefaultPublicCachePolicy( "https://assets.example.test", ), }); await expect(adapter.admin.inspect()).resolves.toMatchObject({ ok: true, value: { ownedCacheCount: 1, unreadableOwnedCacheCount: 1, }, }); // A stream may prefetch one chunk at its high-water mark, but the // consumer must not continue pulling the remaining source. expect(pulls).toBeLessThanOrEqual(3); expect(cancelled).toBe(true); }); it("retains the active and previous verified release for rollback", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bodies = new Map(); const manifests: PublicCacheReleaseManifest[] = []; for (const release of ["release-a", "release-b"]) { const url = `https://assets.example.test/${release}.js`; const bytes = new TextEncoder().encode(release); bodies.set(url, bytes); const asset: PublicCacheAsset = { absoluteUrl: url, expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; manifests.push(await manifestFor(release, [asset], policy)); } const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async (request) => new Response( Uint8Array.from( bodies.get(request.url) ?? new Uint8Array(), ), { headers: { "cache-control": "public, max-age=60", "content-type": "application/javascript", }, }, ), }); for (const manifest of manifests) { expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); expect( await adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); } await cacheStorage.open("ca-public-v1:stale"); const cleaned = await adapter.admin.cleanupOwned(); expect(cleaned).toMatchObject({ ok: true, value: { deletedOwnedCaches: 1, retainedOwnedCaches: 3, }, }); const releaseCaches = (await cacheStorage.keys()).filter((name) => name.startsWith("ca-public-v1:release:"), ); expect(releaseCaches).toHaveLength(2); }); it("requires allowlisted exact query values and rejects duplicates", async () => { const base = createDefaultPublicCachePolicy( "https://assets.example.test", ); const policy: PublicCacheRuntimePolicy = { ...base, allowedQueryParameterNames: ["v"], isQueryParameterValueAllowed: (name, value) => name === "v" && /^r\d+$/u.test(value), }; const bytes = new Uint8Array([1]); const allowed: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/app.js?v=r1", expectedByteLength: 1, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; await expect( manifestFor( "duplicate-query", [ { ...allowed, absoluteUrl: "https://assets.example.test/app.js?v=r1&v=r2", }, ], policy, ), ).rejects.toThrow(); await expect( manifestFor( "signed-query", [ { ...allowed, absoluteUrl: "https://assets.example.test/app.js?sig=secret", }, ], policy, ), ).rejects.toThrow(); }); it("supports exact Vary variants and rejects missing or extra Vary", async () => { const cacheStorage = new MemoryCacheStorage(); const base = createDefaultPublicCachePolicy( "https://assets.example.test", ); const policy: PublicCacheRuntimePolicy = { ...base, allowedVaryHeaderNames: ["accept-language"], }; const url = "https://assets.example.test/messages.json"; const variants = [ { language: "en", bytes: new TextEncoder().encode("hello") }, { language: "ko", bytes: new TextEncoder().encode("안녕") }, ]; const assets = await Promise.all( variants.map(async ({ language, bytes }) => ({ absoluteUrl: url, expectedByteLength: bytes.byteLength, expectedContentType: "application/json", requestHeaders: [["accept-language", language]] as const, integrity: { algorithm: "SHA-256" as const, digestHex: await digestHex(bytes), }, })), ); const manifest = await manifestFor("language-release", assets, policy); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async (request) => { const variant = variants.find( ({ language }) => language === request.headers.get("accept-language"), ); return new Response(variant?.bytes, { headers: { "cache-control": "public, max-age=60", "content-type": "application/json", vary: "Accept-Language", }, }); }, }); expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); expect( await adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); for (const variant of variants) { const match = await adapter.responses.matchActiveExact({ absoluteUrl: url, requestHeaders: [["accept-language", variant.language]], }); expect(match).toMatchObject({ ok: true }); if (!match.ok || !match.value) throw new Error("variant cache miss"); const chunks: number[] = []; for await (const chunk of match.value.body.stream( new AbortController().signal, )) { expect(chunk.ok).toBe(true); if (chunk.ok) chunks.push(...chunk.value); } expect(chunks).toEqual([...variant.bytes]); } const missingVary = createPublicResponseCacheAdapter({ cacheStorage: new MemoryCacheStorage() as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(variants[0]!.bytes, { headers: { "cache-control": "public", "content-type": "application/json", }, }), }); expect( await missingVary.admin.stageRelease( await manifestFor("missing-vary", [assets[0]!], policy), ), ).toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); const noHeaderAsset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/plain.json", expectedByteLength: variants[0]!.bytes.byteLength, expectedContentType: "application/json", integrity: { algorithm: "SHA-256", digestHex: await digestHex(variants[0]!.bytes), }, }; const extraVary = createPublicResponseCacheAdapter({ cacheStorage: new MemoryCacheStorage() as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => new Response(variants[0]!.bytes, { headers: { "cache-control": "public", "content-type": "application/json", vary: "Accept-Language", }, }), }); expect( await extraVary.admin.stageRelease( await manifestFor("extra-vary", [noHeaderAsset], policy), ), ).toMatchObject({ ok: false, error: { code: "POLICY_REJECTED" }, }); }); it("propagates AbortSignal to fetch and cancels a pending body read", async () => { const cacheStorage = new MemoryCacheStorage(); const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([1]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/slow.js", expectedByteLength: 1, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const manifest = await manifestFor("abort-release", [asset], policy); let networkSignal: AbortSignal | undefined; let bodyCancelled = false; let signalCaptured: (() => void) | undefined; const fetchStarted = new Promise((resolve) => { signalCaptured = resolve; }); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async (request) => { networkSignal = request.signal; signalCaptured?.(); return new Response( new ReadableStream({ cancel() { bodyCancelled = true; }, }), { headers: { "cache-control": "public", "content-type": "application/javascript", }, }, ); }, }); const controller = new AbortController(); const staged = adapter.admin.stageRelease(manifest, { signal: controller.signal, }); await fetchStarted; controller.abort(); expect(await staged).toMatchObject({ ok: false, error: { code: "ABORTED" }, }); expect(networkSignal?.aborted).toBe(true); expect(bodyCancelled).toBe(true); expect(await cacheStorage.keys()).toEqual([]); }); }); /** * STO-RR-04 / STO-RR-05. A release cache that is currently serving traffic is * the last thing a repair may destroy. A transient marker read failure is not * evidence of damage, and a repair that has not yet fetched anything has not * yet earned the right to delete what still works. */ describe("public response cache repair is failure-atomic", () => { async function stagedRelease(releaseRegistryId: string) { const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const firstBytes = new Uint8Array([1, 1, 1, 1]); const secondBytes = new Uint8Array([2, 2, 2, 2]); const assets: readonly PublicCacheAsset[] = [ { absoluteUrl: "https://assets.example.test/first.js", expectedByteLength: firstBytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(firstBytes), }, }, { absoluteUrl: "https://assets.example.test/second.js", expectedByteLength: secondBytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(secondBytes), }, }, ]; const bodies = new Map([ [assets[0]!.absoluteUrl, firstBytes], [assets[1]!.absoluteUrl, secondBytes], ]); const cacheStorage = new MemoryCacheStorage(); const fetchLog: string[] = []; let failFrom: string | null = null; const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async (request: Request) => { fetchLog.push(request.url); if (failFrom !== null && request.url === failFrom) { throw new TypeError("network is down"); } const body = bodies.get(request.url); if (!body) throw new TypeError(`unknown asset ${request.url}`); return new Response(Uint8Array.from(body), { headers: { "cache-control": "public", "content-type": "application/javascript", }, }); }, }); const manifest = await manifestFor(releaseRegistryId, assets, policy); expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: true, }); expect( await adapter.admin.activateRelease( manifest.releaseRegistryId, manifest.manifestDigestHex, ), ).toMatchObject({ ok: true }); const cacheName = [...cacheStorage.caches.keys()].find((name) => name.includes(releaseRegistryId), ); if (!cacheName) throw new Error("staged cache missing"); return { adapter, assets, cacheName, cacheStorage, fetchLog, manifest, setFailure(url: string | null) { failFrom = url; }, }; } it("does not delete an active candidate when the marker read fails transiently", async () => { const release = await stagedRelease("transient-marker"); const cache = release.cacheStorage.caches.get(release.cacheName)!; const realMatch = cache.match.bind(cache); let markerReads = 0; const assetUrls = new Set(release.assets.map((asset) => asset.absoluteUrl)); cache.match = async (request: RequestInfo | URL) => { const url = request instanceof Request ? request.url : String(request); if (!assetUrls.has(url)) { markerReads += 1; throw new DOMException("Storage is busy", "InvalidStateError"); } return await realMatch(request); }; const restaged = await release.adapter.admin.stageRelease(release.manifest); expect(markerReads).toBeGreaterThan(0); expect(restaged.ok).toBe(false); expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true); cache.match = realMatch; expect( await release.adapter.responses.matchActiveExact({ absoluteUrl: release.assets[0]!.absoluteUrl, }), ).toMatchObject({ ok: true }); }); it("keeps every healthy asset when one repair fetch fails", async () => { const release = await stagedRelease("partial-repair"); const cache = release.cacheStorage.caches.get(release.cacheName)!; // Corrupt only the first asset's stored bytes. const corrupted = cache.responses.findIndex( (entry) => entry.request.url === release.assets[0]!.absoluteUrl, ); expect(corrupted).toBeGreaterThanOrEqual(0); cache.responses.splice(corrupted, 1); release.setFailure(release.assets[0]!.absoluteUrl); const restaged = await release.adapter.admin.stageRelease(release.manifest); expect(restaged.ok).toBe(false); // The cache still exists and the healthy asset is still served. expect(release.cacheStorage.caches.has(release.cacheName)).toBe(true); expect( await release.adapter.responses.matchActiveExact({ absoluteUrl: release.assets[1]!.absoluteUrl, }), ).toMatchObject({ ok: true }); }); it("still removes a candidate this call created when staging fails", async () => { const policy = createDefaultPublicCachePolicy( "https://assets.example.test", ); const bytes = new Uint8Array([7, 7, 7, 7]); const asset: PublicCacheAsset = { absoluteUrl: "https://assets.example.test/fresh.js", expectedByteLength: bytes.byteLength, expectedContentType: "application/javascript", integrity: { algorithm: "SHA-256", digestHex: await digestHex(bytes), }, }; const cacheStorage = new MemoryCacheStorage(); const adapter = createPublicResponseCacheAdapter({ cacheStorage: cacheStorage as unknown as CacheStorage, crypto: globalThis.crypto, mutationLock: immediateLock, policy, fetcher: async () => { throw new TypeError("network is down"); }, }); const manifest = await manifestFor("fresh-release", [asset], policy); expect(await adapter.admin.stageRelease(manifest)).toMatchObject({ ok: false, }); expect(await cacheStorage.keys()).toEqual([]); }); it("repairs an evicted asset in place and keeps the release usable", async () => { const release = await stagedRelease("in-place-repair"); const cache = release.cacheStorage.caches.get(release.cacheName)!; const evicted = cache.responses.findIndex( (entry) => entry.request.url === release.assets[1]!.absoluteUrl, ); cache.responses.splice(evicted, 1); expect( await release.adapter.admin.stageRelease(release.manifest), ).toMatchObject({ ok: true }); expect( await release.adapter.responses.matchActiveExact({ absoluteUrl: release.assets[1]!.absoluteUrl, }), ).toMatchObject({ ok: true }); expect( await release.adapter.responses.matchActiveExact({ absoluteUrl: release.assets[0]!.absoluteUrl, }), ).toMatchObject({ ok: true }); }); });