diff --git a/CHANGELOG.md b/CHANGELOG.md index 8367e06c..92806972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ authorization and strengthens release compatibility gates. synchronization, file, import, and provider-control writes remain fenced. - Exact Editor qualification preserves immediate revalidation on immutable Pages assets while separately verifying the canonical domain's bounded, - managed four-hour edge cache policy. + managed four-hour policy for edge-cache-eligible asset classes. ## 0.1.0-beta.93 diff --git a/apps/editor/README.md b/apps/editor/README.md index 4dd9debe..157cfd72 100644 --- a/apps/editor/README.md +++ b/apps/editor/README.md @@ -87,10 +87,12 @@ has no target override. Operations must independently record and validate the Pages project, `candidate-b` production branch, canonical domain, and managed Cloudflare account before each qualified release. The immutable Pages origin must serve the repository's asset cache policy exactly. The canonical custom -domain has a separately managed, exact edge transformation to `Cache-Control: -public, max-age=14400, must-revalidate` for `/assets/*`; the manifest remains -`no-store` and all security headers remain exact on both origins. Any other -canonical cache policy fails qualification. If Cloudflare configuration no +domain has a separately managed edge policy: cache-eligible `.css`, `.js`, +`.woff`, and `.woff2` assets must be exactly `Cache-Control: public, +max-age=14400, must-revalidate`, while `.map` and `.wasm` assets retain the +repository's exact immediate-revalidation policy. Unknown asset classes fail +closed. The manifest remains `no-store` and all security headers remain exact +on both origins. Any other canonical cache policy fails qualification. If Cloudflare configuration no longer matches this repository contract, an operator must correct and document that prerequisite before using the command. Do not guess a replacement branch or attach the LAB domain from this script. @@ -131,8 +133,8 @@ Cloudflare's supported root routing is explicit: local `index.html` is fetched at `/`, while other implicit HTML routes are rejected. Pages `_headers` and `_redirects` control files are validated locally rather than fetched. Security headers and the manifest cache policy are checked exactly on both origins. The -immutable origin's asset cache header must match `_headers`; the canonical -origin must match the separately managed exact four-hour edge policy documented +immutable origin's asset cache header must match `_headers`; each canonical +asset must match the separately managed, extension-bounded edge class documented above. The manifest homepage, redirects, Connect origin, and full build revision must all match. In the report contract, `verification.assertions.build_revision` is the boolean diff --git a/scripts/deploy-editor-dev.mjs b/scripts/deploy-editor-dev.mjs index b19c7faf..64438d05 100644 --- a/scripts/deploy-editor-dev.mjs +++ b/scripts/deploy-editor-dev.mjs @@ -653,15 +653,22 @@ function assertRemoteHeaders(headers, path, policy, canonical) { throw new Error("Remote manifest cache policy does not match _headers."); } if (path.startsWith("assets/")) { - const expected = canonical - ? "public, max-age=14400, must-revalidate" - : policy.assets.get("cache-control"); + const sourcePolicy = policy.assets.get("cache-control"); + const expected = canonical ? canonicalAssetCacheControl(path, sourcePolicy) : sourcePolicy; if (headers.get("cache-control") !== expected) { throw new Error(`Remote ${canonical ? "canonical" : "immutable"} asset cache policy is not exact.`); } } } +function canonicalAssetCacheControl(path, sourcePolicy) { + if (/\.(?:css|js|woff2?)$/u.test(path)) { + return "public, max-age=14400, must-revalidate"; + } + if (/\.(?:map|wasm)$/u.test(path)) return sourcePolicy; + throw new Error(`Canonical asset cache class is unsupported for ${path}.`); +} + export function createSuccessReport({ qualification, deployment, wranglerDeployment, wranglerEvidence, verification }) { return { format: "mdbase-editor-lab-deployment/v1", diff --git a/scripts/deploy-editor-dev.test.mjs b/scripts/deploy-editor-dev.test.mjs index 8a729092..3225ba49 100644 --- a/scripts/deploy-editor-dev.test.mjs +++ b/scripts/deploy-editor-dev.test.mjs @@ -585,11 +585,12 @@ test("exact verifier rejects missing, duplicated, omitted, reordered, or non-nor } }); -test("exact verifier requires distinct exact immutable and canonical asset cache policies", async () => { +test("exact verifier requires distinct exact immutable and canonical asset cache classes", async () => { for (const options of [ { immutableAssetCacheControl: "public, max-age=14400, must-revalidate" }, - { canonicalAssetCacheControl: "public, max-age=0, must-revalidate" }, - { canonicalAssetCacheControl: "public, max-age=14400" } + { canonicalEdgeCacheControl: "public, max-age=0, must-revalidate" }, + { canonicalEdgeCacheControl: "public, max-age=14400" }, + { canonicalRevalidatedCacheControl: "public, max-age=14400, must-revalidate" } ]) { const fixture = await deploymentFixture(options); await assert.rejects(verifyExactDeployment({ @@ -597,6 +598,12 @@ test("exact verifier requires distinct exact immutable and canonical asset cache fetchImplementation: fixture.fetch([]) }), /asset cache policy is not exact/u); } + + const unsupported = await deploymentFixture({ extraAssetPath: "assets/data.bin" }); + await assert.rejects(verifyExactDeployment({ + ...unsupported.options, + fetchImplementation: unsupported.fetch([]) + }), /Canonical asset cache class is unsupported/u); }); test("exact verifier rejects unconfigured implicit HTML routes", async () => { @@ -743,7 +750,9 @@ async function deploymentFixture({ redirectEscape = false, missingHeader = null, immutableAssetCacheControl = "public, max-age=0, must-revalidate", - canonicalAssetCacheControl = "public, max-age=14400, must-revalidate" + canonicalEdgeCacheControl = "public, max-age=14400, must-revalidate", + canonicalRevalidatedCacheControl = "public, max-age=0, must-revalidate", + extraAssetPath = null } = {}) { const directory = await mkdtemp(resolve(tmpdir(), "mdbase-editor-dist-")); const canonicalOrigin = "https://editor-lab.mdbase.dev"; @@ -764,10 +773,16 @@ async function deploymentFixture({ const files = new Map([ [".well-known/mdbase-app.json", Buffer.from(`${JSON.stringify(manifest)}\n`)], ["assets/main.js", Buffer.from(`globalThis.revision="${commit}";\n`)], + ["assets/main.js.map", Buffer.from("{}\n")], + ["assets/runtime.wasm", Buffer.from("wasm\n")], + ["assets/styles.css", Buffer.from("body {}\n")], + ["assets/font.woff", Buffer.from("font\n")], + ["assets/font.woff2", Buffer.from("font2\n")], ["index.html", Buffer.from("\n")], ["_headers", Buffer.from(headersText)], ["_redirects", Buffer.from("# no redirects\n")] ]); + if (extraAssetPath) files.set(extraAssetPath, Buffer.from("extra\n")); for (const [path, content] of files) { const target = resolve(directory, path); await mkdir(resolve(target, ".."), { recursive: true }); @@ -799,8 +814,11 @@ async function deploymentFixture({ }; if (path === ".well-known/mdbase-app.json") globalHeaders["cache-control"] = "no-store"; if (path.startsWith("assets/")) { + const canonicalCacheControl = /\.(?:css|js|woff2?)$/u.test(path) + ? canonicalEdgeCacheControl + : canonicalRevalidatedCacheControl; globalHeaders["cache-control"] = url.origin === canonicalOrigin - ? canonicalAssetCacheControl + ? canonicalCacheControl : immutableAssetCacheControl; } if (missingHeader) delete globalHeaders[missingHeader];