Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 3eeec47ffaae52677beca6a0562f031a6af59820
parent eeb9e73b84f04d69ecd8c9c4400b9c81cd629c60
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 12 Sep 2026 11:41:34 -0400

common: a site's digests and duplicates report get CORS — a WIRE change

Every published archive composes `digests/` and (when there is a publishable
cluster) `duplicates.json` into its public dir, and the generated `_headers`
declared neither. On Cloudflare Pages that means exactly one thing: a viewer
on another origin can read the transcripts, the subs, the posts, the
summaries, the stats and the archives of a federated site, and gets a CORS
failure on its digests and its duplicates report.

It survived because nothing local can see it. `serve` — what every e2e run,
the hub suite and the 2-origin suite are served by — hands `**/*.json` a
blanket `Access-Control-Allow-Origin: *` (export/serve.json). The `_headers`
file only exists on the CDN.

    SITE_CORS_PATHS += "/duplicates.json"   (beside the other root JSON)
    SITE_CORS_PATHS += "/digests/*"         (after /posts/*, CONTRACT.layers order)

BEFORE / AFTER, composing the FACTS.md fixture site and diffing `_headers`
against plans/tools/compose-fixture-one-youtube-channel/public/_headers —
four added lines, nothing else moved, not one byte:

    5a6,7
    > /duplicates.json
    >   Access-Control-Allow-Origin: *
    12a15,16
    > /digests/*
    >   Access-Control-Allow-Origin: *

and the composed dir is otherwise IDENTICAL modulo the build clock.

`curl -I` cannot show this, and it is worth writing down why rather than
quoting a run that proves nothing: `wrangler pages dev` — the only local
server here that reads `_headers` at all — adds
`Access-Control-Allow-Origin: *` to EVERY response of its own accord. A file
named in no rule at all comes back with the header (verified against a
zzz-not-in-headers.json dropped into the same dir), so before and after are
indistinguishable over HTTP locally. What wrangler does report is its parse
of the file, and that moves exactly as it should:

    before:  ✨ Parsed 12 valid header rules.
    after:   ✨ Parsed 14 valid header rules.

Cloudflare's own parser, on the composed fixture, counting the two new rules
as valid. The real before/after is a deploy.

The hub's block is unchanged: a hub holds no shard data, composes no digests
and no duplicates report, and gains nothing by declaring them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mcommon/lib/archive/headers.test.ts | 21+++++++++++++++++++++
Mcommon/lib/archive/headers.ts | 31+++++++++++++++++++++++++------
2 files changed, 46 insertions(+), 6 deletions(-)

diff --git a/common/lib/archive/headers.test.ts b/common/lib/archive/headers.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import { HUB_CORS_PATHS, SITE_CORS_PATHS, + contractCorsPaths, renderHeadersFile, } from "./headers"; @@ -15,6 +16,8 @@ const SITE_HEADERS = `# Generated by compose-site.ts — do not edit by hand. Access-Control-Allow-Origin: * /search-aliases.json Access-Control-Allow-Origin: * +/duplicates.json + Access-Control-Allow-Origin: * /summaries/* Access-Control-Allow-Origin: * /subs/* @@ -23,6 +26,8 @@ const SITE_HEADERS = `# Generated by compose-site.ts — do not edit by hand. Access-Control-Allow-Origin: * /posts/* Access-Control-Allow-Origin: * +/digests/* + Access-Control-Allow-Origin: * /stats/* Access-Control-Allow-Origin: * /archives/* @@ -70,6 +75,22 @@ test("renderHeadersFile: the hub block, in full", () => { assert.equal(renderHeadersFile("compose-hub.ts", HUB_CORS_PATHS), HUB_HEADERS); }); +test("the two paths that used to be served without CORS are declared", () => { + // The wire change. A cross-origin viewer could read every other tree and got + // a CORS failure on exactly these two. + assert.ok(SITE_CORS_PATHS.includes("/digests/*")); + assert.ok(SITE_CORS_PATHS.includes("/duplicates.json")); +}); + +test("every contract layer and root file has a CORS line", () => { + for (const p of contractCorsPaths()) { + assert.ok( + SITE_CORS_PATHS.includes(p), + `${p} is on the wire but has no _headers entry`, + ); + } +}); + test("the hub surface is the site surface plus its own pool file", () => { for (const p of HUB_CORS_PATHS) { if (p === "/hub-sites.json") continue; diff --git a/common/lib/archive/headers.ts b/common/lib/archive/headers.ts @@ -7,8 +7,10 @@ // compose-hub.ts) that had already drifted apart. This module is the one // renderer; each compose step supplies the surface it actually serves. // -// Browser-safe like the rest of `lib/archive/`: pure string building, no -// `node:*`. +// Browser-safe like the rest of `lib/archive/`: pure string building over +// `CONTRACT` / `ROOT_FILES`, no `node:*`. + +import { CONTRACT, ROOT_FILES } from "./contract"; // The single header every served document gets. All served data is public // static JSON with no credentials, so `*` is correct and is what lets a @@ -21,17 +23,25 @@ const CORS_HEADER = "Access-Control-Allow-Origin: *"; // Cloudflare, the file is diffed against a committed fixture, and reordering it // would be a wire change with no benefit. // -// NOTE, for the commit that follows this one: this list is today's, verbatim, -// and it is INCOMPLETE — /digests/* and /duplicates.json are composed into -// every site's public dir and declared nowhere. That is a wire change and gets -// its own commit. +// `/digests/*` and `/duplicates.json` were the two gaps — both composed into +// every site's public dir, neither declared here, so a cross-origin viewer got +// a CORS failure on the digest shards and on the duplicates report while every +// other tree read fine. Local `serve` hands `**/*.json` a blanket +// `Access-Control-Allow-Origin: *` (export/serve.json), which is exactly why no +// e2e run ever saw it. +// +// Kept honest by headers.test.ts: every `CONTRACT.layers` tree and every +// `ROOT_FILES` document must appear here, so adding a layer to the contract and +// forgetting its CORS line fails a test rather than a deploy. export const SITE_CORS_PATHS: readonly string[] = [ "/site.json", "/search-aliases.json", + "/duplicates.json", "/summaries/*", "/subs/*", "/transcripts/*", "/posts/*", + "/digests/*", "/stats/*", "/archives/*", "/corpus.json", @@ -73,3 +83,12 @@ export function renderHeadersFile( } return `${out.join("\n")}\n`; } + +// The contract surfaces a site must declare, for the drift test. Exported so +// the assertion lives with the data rather than being re-derived in the test. +export function contractCorsPaths(): string[] { + return [ + ...ROOT_FILES.map((f) => `/${f}`), + ...CONTRACT.layers.map((l) => `/${l}/*`), + ]; +}