import { describe, expect, it } from "vitest"; import type { PublicCacheAsset, PublicCacheReleaseManifest, } from "../../src/application/ports/browser-file-storage/cache-storage-ports.ts"; import { createDefaultPublicCachePolicy, 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("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([]); }); 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([]); }); });