fix: apply the recorded private Cache-Control matrix to image probes

TR-RR-09. The documented BT-IMG-02 contract requires a private response to
carry no-store and fail closed when any directive that describes cacheability
accompanies it. The probe checked only that no-store was present and public was
absent, and the suite pinned the contradictory "private, no-store" as a success.
Both now follow the recorded contract: only no-store and syntactically valid
unknown extensions are admitted, and public, private, immutable, max-age,
s-maxage, no-cache, must-revalidate and proxy-revalidate each fail closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-08-14 14:14:41 +09:00
co-authored by Claude Opus 5
parent efc577de63
commit fb5b449031
2 changed files with 86 additions and 4 deletions
@@ -310,9 +310,13 @@ function validResponseHeaders(
);
if (!directives) return false;
if (request.delivery === "PRIVATE_SIGNED") {
return (
directives.get("no-store") === true &&
!directives.has("public")
// TR-RR-09. The recorded BT-IMG-02 contract is a fail-closed matrix, not a
// pair of checks: a private response must carry `no-store` and nothing else
// that describes cacheability. Only a syntactically valid unknown extension
// is ignored, so a contradictory pairing can never read as acceptable.
if (directives.get("no-store") !== true) return false;
return !PRIVATE_FORBIDDEN_DIRECTIVES.some((name) =>
directives.has(name),
);
}
const maxAge = directives.get("max-age");
@@ -336,6 +340,21 @@ function validResponseHeaders(
);
}
/**
* TR-RR-09. Every cacheability directive a `PRIVATE_SIGNED` response may not
* carry alongside `no-store`.
*/
const PRIVATE_FORBIDDEN_DIRECTIVES: readonly string[] = Object.freeze([
"public",
"private",
"immutable",
"max-age",
"s-maxage",
"no-cache",
"must-revalidate",
"proxy-revalidate",
]);
/**
* BT-IMG-02. Quote- and escape-aware Cache-Control tokenizer.
*
+64 -1
View File
@@ -1658,7 +1658,9 @@ describe("browser image probe", () => {
responseAt(imageUrl, png, {
status: 200,
headers: {
"cache-control": "private, no-store",
// TR-RR-09. A private response carries `no-store` and nothing else
// that describes cacheability.
"cache-control": "no-store",
"content-type": "image/png",
},
}),
@@ -1748,6 +1750,67 @@ describe("browser image probe", () => {
});
});
/**
* TR-RR-09. The recorded BT-IMG-02 contract for a private response is a
* fail-closed matrix. Accepting `no-store` next to a directive that describes
* cacheability lets a self-contradictory policy read as acceptable.
*/
it("applies the full private Cache-Control matrix", async () => {
const png = pngBytes(640, 360);
const probeWith = async (cacheControl: string) => {
const probe = createBrowserImageProbe({
fetcher: (async () =>
responseAt(imageUrl, png, {
status: 200,
headers: {
"cache-control": cacheControl,
"content-type": "image/png",
},
})) as typeof fetch,
createBitmap: async () => ({
width: 640,
height: 360,
close: vi.fn(),
}),
});
return await probe.probe(
request({
delivery: "PRIVATE_SIGNED",
minimumPublicMaxAgeSeconds: 0,
}),
);
};
// Only `no-store`, plus a syntactically valid unknown extension.
expect(await probeWith("no-store")).toMatchObject({ ok: true });
expect(await probeWith('no-store, x-vendor="a,b"')).toMatchObject({
ok: true,
});
for (const companion of [
"public",
"private",
"immutable",
"max-age=60",
"s-maxage=60",
"no-cache",
"must-revalidate",
"proxy-revalidate",
]) {
expect(await probeWith(`no-store, ${companion}`)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
}
for (const withoutNoStore of ["private", "no-cache", "max-age=0"]) {
expect(await probeWith(withoutNoStore)).toMatchObject({
ok: false,
error: { code: "POLICY_REJECTED" },
});
}
});
it("times out a stalled body, aborts the composed signal and cancels its reader", async () => {
const manual = manualImageProbeScheduler();
const cancel = vi.fn(async () => undefined);