feat: park the two policy changes the pricing plan forbids; robots.txt stands in
Build and deploy / build-and-deploy (push) Failing after 4s

The third --apply of 2026-09-04 reached update-distribution and was rejected
atomically: "Distributions with the Free pricing plan can't have the following
features: Custom origin request policy, Custom response headers policy."
Pouya's ruling: both are PARKED as unavailable — a platform constraint, not a
defect.

The pre-flight added in the previous commit could not have caught this, and
that is the point: every limit in PAYLOAD_LIMITS is a property of the payload,
while this is a property of the account, reported only by the call the
pre-flight exists to avoid. Both sections now stop before creating anything.

The plan is not in the CloudFront API — checked across 167 operations, no
operation, shape, member or documentation string mentions one, and
PriceClass_All is the edge-location price class, not the plan. So the gate is a
constant, PLAN_ALLOWS_CUSTOM_POLICIES, and the two sections report as PARKED
under their own heading rather than as skips: the previous commit made a skip
exit 3, and a constraint true on every run would have made 3 permanent. Proven
with a shim that refuses every mutating verb: --apply now makes zero of them.

Substitute (a): Disallow: /pouya-lajevardi-bio.pdf in robots.txt, placed before
Allow:/ so first-match crawlers honour it too. It is not an equivalent and the
file says so — it stops the PDF being fetched, solving the duplicate-of-/bio/
problem, but does not de-index a URL linked from /bio/ and /about/. Verified:
syntax, a match simulation under both crawler semantics, and that the sitemap
does not list the PDF.

Substitute (b): the WAF web ACL CreatedByCloudFront-f8fbf256 is already
attached — 925 WCU, three AWS managed rule groups, no rate-based statement.
That corrects §9 Q65, which framed WAF as a cost decision about adding one and
named the now-unappliable header forwarding as its groundwork. The real
question is one rule on an ACL already paid for, and a rate-based rule matches
the viewer address directly, so the capability is superseded rather than lost.

Reviewed in two rounds by me rather than a separate agent, per instruction.

Nothing was applied to the distribution and nothing was deployed; robots.txt
needs one site deploy.

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-04 14:52:13 -04:00
co-authored by Claude Opus 5
parent 07a8ff6989
commit 17e316dc1d
6 changed files with 376 additions and 41 deletions
+62 -6
View File
@@ -1,6 +1,9 @@
/**
* Applies the five distribution changes the site needs, as one reviewable
* transaction. `docs/09-cutover-runbook.md` Part 3 is what calls it.
* Applies the distribution changes the site needs, as one reviewable
* transaction. ⚠️ **THREE OF THE FIVE ARE APPLICABLE; 4 AND 5 ARE PARKED** —
* the pricing plan forbids both a custom response headers policy and a custom
* origin request policy, so they are reported and skipped rather than attempted.
* See `PLAN_ALLOWS_CUSTOM_POLICIES` below and `AGENTS.md` §7. `docs/09-cutover-runbook.md` Part 3 is what calls it.
*
* 1. FunctionAssociations on the default behaviour -> `router.js`, viewer
* request. Without it 22 of 23 pages return S3's AccessDenied XML.
@@ -494,7 +497,42 @@ function findPdfPolicy() {
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) {
/* ⚠️ PARKED, NOT SKIPPED, AND THE DISTINCTION IS THE EXIT STATUS. A skip means
something unexpected happened and someone should look; these two are a
standing platform constraint that will be true on every run until the pricing
plan changes. Counting them as skips would make `exit 3` permanent, and a
signal that is always on is not a signal. */
const parked = [];
/**
* 🛑 **SECTIONS 4 AND 5 CANNOT BE APPLIED ON THIS DISTRIBUTION'S PRICING PLAN.**
* The third `--apply` of 2026-09-04 reached `update-distribution` and was
* rejected atomically:
*
* Distributions with the Free pricing plan can't have the following features:
* Custom origin request policy, Custom response headers policy
*
* ⚠️ **AND IT FAILED AT THE LAST CALL, AFTER BOTH POLICIES HAD BEEN CREATED** —
* the whole point of the pre-flight is to fail before that, so both sections now
* stop here instead. `AGENTS.md` §7 records the plan; `docs/06` closes both
* items; `docs/09` Part 3 has all three attempts.
*
* ⚠️ **THIS IS A CONSTANT AND NOT A PROBE, BECAUSE THE PRICING PLAN IS NOT IN
* THE API.** Checked 2026-09-04 against the CloudFront model: 167 operations,
* and not one shape, member or documentation string mentions a pricing plan.
* `PriceClass_All` on this distribution is the EDGE-LOCATION price class, a
* different and much older concept — do not read it as the plan. The only
* signal AWS gives is the `update-distribution` rejection itself, which is the
* thing this exists to avoid. So: flip this to `true` when the plan changes,
* and the two sections come back exactly as they were.
*/
const PLAN_ALLOWS_CUSTOM_POLICIES = false;
if (!PLAN_ALLOWS_CUSTOM_POLICIES) {
parked.push(
`${PDF_PATTERN} / ${PDF_POLICY_NAME} — a custom response headers policy is not available on this distribution's pricing plan. The stand-in is \`Disallow: /pouya-lajevardi-bio.pdf\` in public/robots.txt`,
);
} else 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`,
);
@@ -952,7 +990,11 @@ const apiBehaviour = (cfg.CacheBehaviors?.Items ?? []).find(
(b) => b.PathPattern === PATH_PATTERN,
);
if (!apiBehaviour) {
if (!PLAN_ALLOWS_CUSTOM_POLICIES) {
parked.push(
`${PATH_PATTERN} / ${ORP_NAME} — a custom origin request policy is not available on this distribution's pricing plan. Superseded rather than merely parked: the WAF web ACL already attached to this distribution is where a per-IP rule belongs (AGENTS.md §7)`,
);
} else if (!apiBehaviour) {
/* Unreachable in practice — section 3 either found it or pushed it — so if it
fires, something above changed. Skip rather than throw, for the reason
section 4 gives: sections 1-3 have already staged their mutations. */
@@ -1160,6 +1202,18 @@ console.log('');
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 (parked.length) {
console.log(
`· ${parked.length} thing(s) PARKED — not available on this distribution's pricing plan:`,
);
for (const k of parked) console.log(` ${k}`);
console.log(
' This is expected and does not affect the exit status. AGENTS.md §7',
);
console.log(' records the plan; flip PLAN_ALLOWS_CUSTOM_POLICIES if it');
console.log(' changes. Sections 1-3 are unaffected.');
console.log('');
}
if (skipped.length) {
console.log(`${skipped.length} thing(s) SKIPPED, not changed:`);
for (const k of skipped) console.log(` ! ${k}`);
@@ -1182,8 +1236,10 @@ const exitCode = skipped.length ? EXIT_SKIPPED : 0;
if (changes.length === 0) {
console.log(
skipped.length
? 'NOTHING TO CHANGE — but see the skips above; the distribution does NOT carry all five.'
: 'NOTHING TO CHANGE — the distribution already carries all five.',
? 'NOTHING TO CHANGE — but see the skips above; the distribution does NOT carry everything this script manages.'
: parked.length
? 'NOTHING TO CHANGE — the distribution carries everything this script can apply on the current pricing plan. The parked items above are not among them.'
: 'NOTHING TO CHANGE — the distribution already carries all five.',
);
process.exit(exitCode);
}