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:
co-authored by
Claude Opus 5
parent
efc577de63
commit
fb5b449031
@@ -310,9 +310,13 @@ function validResponseHeaders(
|
|||||||
);
|
);
|
||||||
if (!directives) return false;
|
if (!directives) return false;
|
||||||
if (request.delivery === "PRIVATE_SIGNED") {
|
if (request.delivery === "PRIVATE_SIGNED") {
|
||||||
return (
|
// TR-RR-09. The recorded BT-IMG-02 contract is a fail-closed matrix, not a
|
||||||
directives.get("no-store") === true &&
|
// pair of checks: a private response must carry `no-store` and nothing else
|
||||||
!directives.has("public")
|
// 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");
|
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.
|
* BT-IMG-02. Quote- and escape-aware Cache-Control tokenizer.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1658,7 +1658,9 @@ describe("browser image probe", () => {
|
|||||||
responseAt(imageUrl, png, {
|
responseAt(imageUrl, png, {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
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",
|
"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 () => {
|
it("times out a stalled body, aborts the composed signal and cancels its reader", async () => {
|
||||||
const manual = manualImageProbeScheduler();
|
const manual = manualImageProbeScheduler();
|
||||||
const cancel = vi.fn(async () => undefined);
|
const cancel = vi.fn(async () => undefined);
|
||||||
|
|||||||
Reference in New Issue
Block a user