fix: refute (ar)'s intake finding; fix the D20 gloss class; add X-Robots-Tag on *.pdf
Build and deploy / build-and-deploy (push) Failing after 4s

Pouya's rulings of 2026-09-03, in five parts.

1. THE INTAKE FORM IS NOT BROKEN. (ar) was wrong. docs/09 §7.1 verbatim —
POST /api/intake with an Origin header — returns 303 to /contact/could-not-send/
with access-control-allow-origin echoed; the same probe without Origin returns
403. A bare POST 403s BY DESIGN and §7.1 says so three lines below the probe it
prescribes: "403 means the Origin header did not arrive". The earlier finding
read a status code without reading the document that defines it. Second time in
two days. CLAUDE.md's instrument list goes eight to nine. D20 findings 12 and 19
fall with it; §7.2 (that both emails arrive) is still owed.

The correction is APPENDED as entry (as); (ar) stands unedited.

2. The privacy retention comment was stale, not a defect — superseded by his
decision to publish and confirm after launch, reading from 2026-09-04. Reworded;
the TODO(pouya) came off with the gate it enforced. The mechanism finding
survives: it was a JSX comment, stripped by Astro, so no build or deploy path
could see it. A publication gate that lives only in a stripped comment is not a
gate. §9 Q60 corrected.

3. The gloss class is fixed — 15 of the 20 D20 findings, 14 distinct edits across
9 files, under the rule "the gloss may say no more than the extract says; no new
claims, no new sources". Swept three unpublished insights drafts too, and
corrected the wrong CAA attribution at its source in docs/reference/, which is
where a fixed page re-seeds. /bio/ changed, so the committed PDF is regenerated
(89,549 B, 1 page asserted). Three findings outstanding: 10 needs a ruling, 11 is
ruled and owed via Q60, 13 needs him to have said it. R1 is not one of the twenty.

4. X-Robots-Tag cannot be done with S3 object metadata — --metadata writes user
metadata, returned as x-amz-meta-x-robots-tag, which no crawler reads. Built as
the CloudFront response-headers policy docs/06 has specified all along:
configure.mjs section 4. It needs a --apply run, not a deploy. The policy is
cloned from whatever is attached at run time and reconciled on every run, because
a response-headers policy replaces rather than merges.

5. Headshot deferred as an open non-defect. The master and the srcset ladder are
both fine; Astro passes no quality, so AVIF encodes at sharp's default 50 and is
served first.

Two review rounds, 29 findings, all resolved, none declined; stopped at two per
D19. NINE of round 2's fourteen were defects in round 1's own repairs — including
a fix that harmonised both /fees/ rows onto wording that was itself unregistered,
publishing an unsourced fee term twice where it had been once.

Gates, exit status read for each: check 0 (0 errors, 0 warnings, 0 hints),
build 0 (23 pages), check:claims 0, check:intake 0, og:proof 0, lint 0, minifier
grep exit 1, router.test.mjs 30/30. Lighthouse NOT run. Nothing deployed and
nothing applied to the distribution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-03 17:23:20 -04:00
co-authored by Claude Opus 5
parent b9523817e2
commit 02739adac9
18 changed files with 764 additions and 101 deletions
+298 -2
View File
@@ -1,5 +1,5 @@
/**
* Applies the three distribution changes the site needs, as one reviewable
* Applies the four distribution changes the site needs, as one reviewable
* transaction. `docs/09-cutover-runbook.md` Part 3 is what calls it.
*
* 1. FunctionAssociations on the default behaviour -> `router.js`, viewer
@@ -9,6 +9,9 @@
* "the single most common misconfiguration in this stack".
* 3. A `/api/*` cache behaviour on a new origin pointing at the HTTP API, so
* the intake form's same-origin POST reaches the handler.
* 4. A `*.pdf` cache behaviour carrying a response-headers policy that adds
* `X-Robots-Tag: noindex`, so the bio PDF is not indexed as a duplicate of
* `/bio/`. `docs/06`'s checklist item carries the reasoning.
*
* ⚠️ DRY RUN BY DEFAULT. It prints what it would change and exits 0 without
* calling `update-distribution`. `--apply` is the only thing that writes, and it
@@ -291,10 +294,303 @@ if (catchAll !== -1 && catchAll < apiIndex) {
);
}
/* ---- 4. X-Robots-Tag: noindex on the bio PDF ----------------------------
⚠️ S3 OBJECT METADATA CANNOT DO THIS. `aws s3 sync --metadata` writes USER
metadata, which S3 returns as `x-amz-meta-x-robots-tag` — a header no crawler
reads. Only a literal `X-Robots-Tag` counts and the REST endpoint will not
emit one, so the mechanism is a response-headers policy. `docs/06`'s checklist
item carries why the PDF needs it at all; this comment carries only what the
next implementer needs in order not to break it.
⚠️ THE ONE LIVE CONSTRAINT: A RESPONSE-HEADERS POLICY REPLACES, IT DOES NOT
MERGE. Attaching a policy to `*.pdf` means the default behaviour's policy no
longer applies there, so this one must carry everything that policy carries —
hence the clone below, and hence the drift check that follows it. Measured
2026-09-03: all five security headers arrive on the live PDF today. */
const PDF_PATTERN = '*.pdf';
const PDF_POLICY_NAME = 'adr-sml-pdf-noindex';
const XRT = { Header: 'X-Robots-Tag', Value: 'noindex', Override: true };
const defaultRhpId = cfg.DefaultCacheBehavior.ResponseHeadersPolicyId;
function getResponseHeadersPolicy(id) {
return aws([
'cloudfront',
'get-response-headers-policy',
'--id',
id,
'--output',
'json',
]);
}
/* Only `custom` is listed: `adr-sml-pdf-noindex` is a name this script creates,
so a managed hit is impossible and listing them would be a wasted call that
reads as if one were possible. */
function findPdfPolicy() {
const res = aws([
'cloudfront',
'list-response-headers-policies',
'--type',
'custom',
'--output',
'json',
]);
const items = res?.ResponseHeadersPolicyList?.Items ?? [];
return (
items.find(
(i) =>
i.ResponseHeadersPolicy.ResponseHeadersPolicyConfig.Name ===
PDF_POLICY_NAME,
)?.ResponseHeadersPolicy ?? null
);
}
/* ⚠️ SKIP, DO NOT THROW. Sections 13 have already staged their mutations, and
throwing here would make the script unusable for re-applying the router
function or the 404 mapping — which is the re-run contract this file promises
at the top, and `router.js` is what keeps 22 of 23 pages off S3's
AccessDenied. A missing policy on the default behaviour is section 4's
problem alone. */
/* ⚠️ A SKIP IS NOT A CHANGE AND MUST NOT ENTER `changes`. That array is printed
under "N change(s)", `docs/09` Part 3 tells the operator to COUNT those lines,
and the `NOTHING TO CHANGE` guard exits on its length — so a skip in there
would both miscount and send an `update-distribution` carrying a config
nothing mutated. Skips get their own list and their own heading. */
const skipped = [];
if (!defaultRhpId) {
skipped.push(
`${PDF_PATTERN} / ${PDF_POLICY_NAME} — the default behaviour has no ResponseHeadersPolicyId, so there is nothing to clone the security headers from`,
);
} else {
const existingPdfPolicy = findPdfPolicy();
let pdfPolicyId = existingPdfPolicy?.Id ?? null;
/* ⚠️ RECONCILE ON EVERY RUN, NEVER ONLY AT CREATION. The clone is a copy of a
fact that lives somewhere else, so it goes stale the moment the default
behaviour's policy changes — and it would go stale silently, as a uniform
pass. `docs/05` already specifies a Content-Security-Policy (a field OF
SecurityHeadersConfig) and a Permissions-Policy (which can only be a CUSTOM
header) that the site does not ship yet; adding either to the default
behaviour would reach the pages and not the PDF. This check fails loudly
instead, naming the diff. */
const source = getResponseHeadersPolicy(defaultRhpId);
const srcCfg = source?.ResponseHeadersPolicy?.ResponseHeadersPolicyConfig;
/* ⚠️ SKIP, NOT THROW — same rule as the missing-id case above, and it was
inconsistent for one round. A policy carrying only `CorsConfig` is legal;
an ABSENT source is section 4's problem alone and must not stop sections
1-3 from re-applying `router.js`. The DRIFT throw below is different: that
is a divergence, not an absence, and `docs/09` Part 3 argues for it. */
if (!srcCfg?.SecurityHeadersConfig) {
skipped.push(
`${PDF_PATTERN} / ${PDF_POLICY_NAME} — response-headers policy ${defaultRhpId} has no SecurityHeadersConfig to clone`,
);
} else {
const wanted = {
SecurityHeadersConfig: srcCfg.SecurityHeadersConfig,
...(srcCfg.CorsConfig ? { CorsConfig: srcCfg.CorsConfig } : {}),
...(srcCfg.RemoveHeadersConfig
? { RemoveHeadersConfig: srcCfg.RemoveHeadersConfig }
: {}),
...(srcCfg.ServerTimingHeadersConfig
? { ServerTimingHeadersConfig: srcCfg.ServerTimingHeadersConfig }
: {}),
CustomHeadersConfig: {
Quantity: (srcCfg.CustomHeadersConfig?.Items ?? []).length + 1,
Items: [...(srcCfg.CustomHeadersConfig?.Items ?? []), XRT],
},
};
if (existingPdfPolicy) {
const have = existingPdfPolicy.ResponseHeadersPolicyConfig;
const norm = (o) => JSON.stringify(o ?? null);
const drift = [
'SecurityHeadersConfig',
'CorsConfig',
'RemoveHeadersConfig',
'ServerTimingHeadersConfig',
]
.filter((k) => norm(have[k]) !== norm(wanted[k]))
.concat(
norm(have.CustomHeadersConfig?.Items) !==
norm(wanted.CustomHeadersConfig.Items)
? ['CustomHeadersConfig']
: [],
);
if (drift.length) {
/* Print BOTH SIDES of every drifted key. Naming the field alone does not
tell the operator which header moved, nor which direction to reconcile
in — the same message fires whether the source gained a header or the
PDF policy lost its X-Robots-Tag, and those need opposite repairs. */
const detail = drift
.map(
(k) =>
` ${k}\n pdf policy : ${norm(
k === 'CustomHeadersConfig'
? have.CustomHeadersConfig?.Items
: have[k],
)}\n default : ${norm(
k === 'CustomHeadersConfig'
? wanted.CustomHeadersConfig.Items
: wanted[k],
)}`,
)
.join('\n');
throw new Error(
`${PDF_POLICY_NAME} has DRIFTED from the default behaviour's policy ` +
`${defaultRhpId} on ${drift.length} field(s). The PDF is being served ` +
`different headers from the pages — read which way before repairing:\n` +
`${detail}\n` +
`Reconcile with update-response-headers-policy (it needs the policy's ` +
`own ETag), then re-run. This script will not silently paper over it.`,
);
}
console.log(
`· response-headers policy ${PDF_POLICY_NAME} exists and matches the default behaviour`,
);
} else if (!APPLY) {
console.log(`· would CREATE response-headers policy ${PDF_POLICY_NAME}`);
changes.push(
`create response-headers policy ${PDF_POLICY_NAME} (SecurityHeadersConfig cloned from ${defaultRhpId} + X-Robots-Tag: noindex)`,
);
} else {
const created = aws([
'cloudfront',
'create-response-headers-policy',
'--response-headers-policy-config',
JSON.stringify({
Name: PDF_POLICY_NAME,
Comment:
'Cloned from the default behaviour, plus X-Robots-Tag: noindex for *.pdf. See infra/cloudfront/configure.mjs section 4.',
...wanted,
}),
'--output',
'json',
]);
pdfPolicyId = created?.ResponseHeadersPolicy?.Id;
if (!pdfPolicyId) {
throw new Error('create-response-headers-policy returned no Id');
}
console.log(
`created response-headers policy ${PDF_POLICY_NAME} = ${pdfPolicyId}`,
);
changes.push(
`created response-headers policy ${PDF_POLICY_NAME} = ${pdfPolicyId}`,
);
}
const pdfBehaviours = cfg.CacheBehaviors?.Items ?? [];
const foundPdf = pdfBehaviours.find((b) => b.PathPattern === PDF_PATTERN);
if (foundPdf) {
/* ⚠️ PRESENCE IS NOT CORRECTNESS. This checked only that a `*.pdf`
behaviour existed, so one added by hand — while chasing the
`aws s3 sync --metadata` route this file's header records as the original
instruction — would report `already present`, push nothing, and print
NOTHING TO CHANGE while the PDF served no `X-Robots-Tag` at all. Section 1
compares the FunctionARN before declaring a match; so does this now. */
const wrong = [];
if (foundPdf.ResponseHeadersPolicyId !== pdfPolicyId) {
wrong.push(
`ResponseHeadersPolicyId is ${foundPdf.ResponseHeadersPolicyId ?? '(none)'}, expected ${pdfPolicyId ?? '(the policy this script manages)'}`,
);
}
if (foundPdf.TargetOriginId !== cfg.DefaultCacheBehavior.TargetOriginId) {
wrong.push(
`TargetOriginId is ${foundPdf.TargetOriginId}, expected ${cfg.DefaultCacheBehavior.TargetOriginId}`,
);
}
const hasViewerRequest = (
foundPdf.FunctionAssociations?.Items ?? []
).some((i) => i.EventType === 'viewer-request');
if (!hasViewerRequest) {
wrong.push(
'no viewer-request FunctionAssociation — router.js normalises `//` and `\\` on file paths, so `//pouya-lajevardi-bio.pdf` would 404 instead of 301',
);
}
if (wrong.length) {
throw new Error(
`a ${PDF_PATTERN} cache behaviour already exists but is NOT the one this ` +
`script manages:\n - ${wrong.join('\n - ')}\n` +
`Reconcile or remove it before re-running; this script will not adopt ` +
`a behaviour it cannot account for.`,
);
}
console.log(
`· cache behaviour ${PDF_PATTERN} already present and correctly configured`,
);
} else {
const d = cfg.DefaultCacheBehavior;
const behaviour = {
PathPattern: PDF_PATTERN,
TargetOriginId: d.TargetOriginId,
ViewerProtocolPolicy: d.ViewerProtocolPolicy,
AllowedMethods: d.AllowedMethods,
CachePolicyId: d.CachePolicyId,
/* Placeholder only in a dry run — the real id exists by the time --apply
reaches this line, because the branch above created it. */
ResponseHeadersPolicyId: pdfPolicyId ?? '<created on --apply>',
Compress: d.Compress,
SmoothStreaming: false,
FieldLevelEncryptionId: '',
/* ⚠️ THE ROUTER FUNCTION IS ATTACHED, AND IT IS NOT A NO-OP ON FILE PATHS.
`router.js` normalises `\` to `/` and collapses a leading `//` run
BEFORE it tests for an extension, and 301s when normalisation changed
anything — so `//pouya-lajevardi-bio.pdf` redirects to the canonical
path today. Omitting the association here would silently drop that and
hand S3 the doubled key instead. The `/api/*` reason for omitting it —
a 301 turning a POST into a GET and losing the body — does not apply to
a GET-only PDF. */
FunctionAssociations: d.FunctionAssociations ?? { Quantity: 0 },
LambdaFunctionAssociations: { Quantity: 0 },
TrustedKeyGroups: { Enabled: false, Quantity: 0 },
};
/* ⚠️ STAGE THE REPORT EVEN WHEN THE ID IS NOT KNOWN YET. The dry run's whole
job is to show what would touch a distribution serving 23 pages; reporting
only the harmless policy creation and staying silent about the behaviour
would mean the first sight of it is `update-distribution` writing it. The
`cfg` mutation stays gated on a real id; the REPORT does not. */
changes.push(
`CacheBehaviors += ${PDF_PATTERN} -> ${d.TargetOriginId}, default cache policy, ${PDF_POLICY_NAME}` +
(pdfPolicyId
? ` (${pdfPolicyId})`
: ' (policy id created in the same --apply pass)'),
);
if (!APPLY && !pdfPolicyId) {
console.log(
`· would ADD cache behaviour ${PDF_PATTERN}:\n` +
JSON.stringify(behaviour, null, 2)
.split('\n')
.map((l) => ' ' + l)
.join('\n'),
);
} else {
pdfBehaviours.push(behaviour);
cfg.CacheBehaviors = {
Quantity: pdfBehaviours.length,
Items: pdfBehaviours,
};
}
}
}
}
console.log('');
/* Skips print under their own heading and are NOT counted as changes — see the
comment on `skipped`. A skip means section 4 did nothing and the PDF is
probably not noindexed; that is louder than a silent omission and quieter
than a false change. */
if (skipped.length) {
console.log(`${skipped.length} thing(s) SKIPPED, not changed:`);
for (const k of skipped) console.log(` ! ${k}`);
console.log(' Sections 1-3 are unaffected. Investigate before relying on');
console.log(` ${PDF_PATTERN} carrying X-Robots-Tag.`);
console.log('');
}
if (changes.length === 0) {
console.log(
'NOTHING TO CHANGE — the distribution already carries all three.',
skipped.length
? 'NOTHING TO CHANGE — but see the skips above; the distribution does NOT carry all four.'
: 'NOTHING TO CHANGE — the distribution already carries all four.',
);
process.exit(0);
}
+8 -1
View File
@@ -1,5 +1,12 @@
/**
* CloudFront Function, VIEWER REQUEST, on the default cache behaviour only.
* CloudFront Function, VIEWER REQUEST, on the DEFAULT behaviour and on `*.pdf`.
* Not on `/api/*` — see the rule below, which is the one that matters.
*
* `*.pdf` has it because this function is NOT a no-op on file paths: it
* normalises `\` to `/` and collapses a leading `//` run BEFORE the extension
* test, and 301s when that changed anything. Measured live 2026-09-03:
* `//pouya-lajevardi-bio.pdf` returns 301. Dropping the association there hands
* S3 the doubled key and returns 404 instead.
*
* ⚠️ THE SITE DOES NOT WORK WITHOUT THIS. `astro.config.mjs` sets
* `trailingSlash: 'always'` and `build.format: 'directory'`, so every route is