feat: production run — Q61 ramp, /404/, CloudFront router, cutover runbook
Build and deploy / build-and-deploy (push) Failing after 4s
Build and deploy / build-and-deploy (push) Failing after 4s
Five items of Pouya's production run, 2026-09-01.
Q61 — scroll-padding-top becomes a max() ramp on `10lh - 83px`, with the
plain calc() first as the fallback for engines without `lh`. Hidden focus
stops under minimumFontSize=32: 290 of 1,455 -> 0, control build still
290. Default settings byte-identical (0 differences over 352 page-widths x
17 fields). The 12 residual cells at minimumFontSize=16/20 are pre-existing
and unchanged-or-better; reported, not widened, per instruction.
Intake backend + CloudFront — docs/09-cutover-runbook.md is the
copy-paste sequence for admin execution: every command followed by its
verification and expected output, rollback per part, and Part 10 is Q60's
TTL test. infra/cloudfront/router.js is the trailing-slash function
(30-case suite; 8 fail against the pre-review version, incl. a
protocol-relative open redirect). infra/cloudfront/configure.mjs is
dry-run-by-default and idempotent. scripts/intake-env.mjs emits the six
Lambda env vars from src/data/site.ts.
Four launch blockers found by reading the running system:
- handler.mjs wrote pk/sk; the live table's key is submissionId with no
sort key, so every submission would have failed validation silently
- the Lambda invoke permission is scoped to the old route path
- 22 of 23 pages 403 without the router function
- there was no 404 page; src/pages/404.astro adds it
Claims audit (D20 cutover pass) — five gloss over-reaches corrected on
/practice/energy/, /practice/insurance/ (x2), /practice/technology/ and
/med-arb/. Three findings left open for Pouya: Q62, the /med-arb/ gloss,
and Q60.
Q62 — one frozen-tripwire pattern added under the freeze's own breach
exception, with a probe and four negative fixtures. check:claims exits 1
until the false /legal/privacy/ sentence is corrected, so both deploy
paths are blocked by a mechanism rather than by memory.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
co-authored by
Claude Opus 5
parent
ca1c2524e1
commit
bd282aa47d
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Unit test for the viewer-request router. `node infra/cloudfront/router.test.mjs`.
|
||||
*
|
||||
* The function file cannot use module syntax — CloudFront's runtime has no
|
||||
* `export` — so it is read and evaluated rather than imported. `aws cloudfront
|
||||
* test-function` is the authoritative check because it runs the real runtime;
|
||||
* this one runs in a second, catches the branch mistakes, and costs nothing.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const src = readFileSync(join(here, 'router.js'), 'utf8');
|
||||
const handler = new Function(`${src}; return handler;`)();
|
||||
|
||||
const req = (uri, querystring = {}) => ({ request: { uri, querystring } });
|
||||
|
||||
const CASES = [
|
||||
// [uri, querystring, expected] — expected is {uri} for a rewrite/passthrough
|
||||
// or {status, location} for a redirect.
|
||||
['/', {}, { uri: '/index.html' }],
|
||||
['/about/', {}, { uri: '/about/index.html' }],
|
||||
['/practice/construction/', {}, { uri: '/practice/construction/index.html' }],
|
||||
['/contact/received/', {}, { uri: '/contact/received/index.html' }],
|
||||
['/about', {}, { status: 301, location: '/about/' }],
|
||||
['/practice/energy', {}, { status: 301, location: '/practice/energy/' }],
|
||||
// Files are untouched — every one of these is a real object in dist/.
|
||||
['/robots.txt', {}, { uri: '/robots.txt' }],
|
||||
['/sitemap-index.xml', {}, { uri: '/sitemap-index.xml' }],
|
||||
['/404.html', {}, { uri: '/404.html' }],
|
||||
['/favicon.ico', {}, { uri: '/favicon.ico' }],
|
||||
['/pouya-lajevardi-bio.pdf', {}, { uri: '/pouya-lajevardi-bio.pdf' }],
|
||||
['/_astro/schema.Cm5su60K.css', {}, { uri: '/_astro/schema.Cm5su60K.css' }],
|
||||
['/og/mediation.jpg', {}, { uri: '/og/mediation.jpg' }],
|
||||
// The query string survives the redirect, normalised to `name=value`.
|
||||
[
|
||||
'/fees',
|
||||
{ utm_source: { value: 'linkedin' }, ref: { value: '' } },
|
||||
{ status: 301, location: '/fees/?utm_source=linkedin&ref=' },
|
||||
],
|
||||
/* ⚠️ THE OPEN-REDIRECT CASES. CloudFront forwards duplicate leading slashes
|
||||
verbatim (it collapses dot-segments but not `//`), so without normalisation
|
||||
`//evil.example.com/x` produced `Location: //evil.example.com/x/` — a
|
||||
network-path reference that sends the viewer to another host from this
|
||||
domain's own URL. The backslash form defeats a `startsWith('//')` guard,
|
||||
because the URL Standard maps `\` to `/` in special schemes. Both must stay
|
||||
same-origin, and both must keep a SINGLE leading slash. */
|
||||
[
|
||||
'//evil.example.com/x',
|
||||
{},
|
||||
{ status: 301, location: '/evil.example.com/x/' },
|
||||
],
|
||||
[
|
||||
'///evil.example.com/x',
|
||||
{},
|
||||
{ status: 301, location: '/evil.example.com/x/' },
|
||||
],
|
||||
[
|
||||
'/\\evil.example.com/x',
|
||||
{},
|
||||
{ status: 301, location: '/evil.example.com/x/' },
|
||||
],
|
||||
/* ⚠️ A NORMALISED PATH IS REDIRECTED, NOT REWRITTEN — this asserted a 200 for
|
||||
one revision, which closed the redirect and opened an unbounded family of
|
||||
duplicate URLs for every page on the site. */
|
||||
[
|
||||
'//evil.example.com/x/',
|
||||
{},
|
||||
{ status: 301, location: '/evil.example.com/x/' },
|
||||
],
|
||||
['//about/', {}, { status: 301, location: '/about/' }],
|
||||
['///about/', {}, { status: 301, location: '/about/' }],
|
||||
['/\\about/', {}, { status: 301, location: '/about/' }],
|
||||
/* A file is normalised too. This branch returned `request` untouched for one
|
||||
revision, so `//robots.txt` reached S3 with the doubled slash and 404'd. */
|
||||
['//robots.txt', {}, { status: 301, location: '/robots.txt' }],
|
||||
['/\\robots.txt', {}, { status: 301, location: '/robots.txt' }],
|
||||
/* An interior `//` is left alone on purpose: it is a key that does not exist,
|
||||
so it resolves to the 404 page. Only the leading run is a security question. */
|
||||
['/a//b/', {}, { uri: '/a//b/index.html' }],
|
||||
/* Header-injection surface: CR, LF, space and the delimiters browsers disagree
|
||||
about are stripped rather than re-encoded — an already-encoded value must not
|
||||
be encoded twice. `%20` therefore passes through untouched. */
|
||||
[
|
||||
'/fees',
|
||||
{ q: { value: 'a b"><x' }, utm: { value: 'a%20b' } },
|
||||
{ status: 301, location: '/fees/?q=abx&utm=a%20b' },
|
||||
],
|
||||
[
|
||||
'/fees',
|
||||
{ evil: { value: 'x\r\nSet-Cookie: a=b' } },
|
||||
{ status: 301, location: '/fees/?evil=xSet-Cookie:a=b' },
|
||||
],
|
||||
/* `#` changes the STRUCTURE of the Location — without stripping it, `&b=y`
|
||||
lands in a fragment and the parameter is silently lost. */
|
||||
[
|
||||
'/fees',
|
||||
{ a: { value: 'x#&b=y' } },
|
||||
{ status: 301, location: '/fees/?a=x&b=y' },
|
||||
],
|
||||
/* ⚠️ AND THESE MUST SURVIVE. `| ^ ` { }` are not in WHATWG's query
|
||||
percent-encode set, so a browser sends them raw — and `|` is routine in
|
||||
ad-platform tracking values. One revision of `safe()` stripped all of them,
|
||||
silently corrupting exactly the campaign links the 301 exists to preserve. */
|
||||
[
|
||||
'/fees',
|
||||
{ utm_content: { value: 'banner|top' }, k: { value: 'a{b}c^d`e' } },
|
||||
{ status: 301, location: '/fees/?utm_content=banner|top&k=a{b}c^d`e' },
|
||||
],
|
||||
// multiValue, which no case exercised before.
|
||||
[
|
||||
'/fees',
|
||||
{ tag: { value: 'a', multiValue: [{ value: 'a' }, { value: 'b' }] } },
|
||||
{ status: 301, location: '/fees/?tag=a&tag=b' },
|
||||
],
|
||||
/* /api/intake must NEVER be redirected — a 301 turns a POST into a GET and
|
||||
the submission body is gone. This function is not associated with the
|
||||
/api/* behaviour, so this case documents WHY the association matters: if it
|
||||
ever were associated, this is the damage. */
|
||||
['/api/intake', {}, { status: 301, location: '/api/intake/' }],
|
||||
];
|
||||
|
||||
let pass = 0;
|
||||
const failures = [];
|
||||
for (const [uri, qs, expected] of CASES) {
|
||||
const out = handler(req(uri, qs));
|
||||
let actual;
|
||||
if (out.statusCode) {
|
||||
actual = { status: out.statusCode, location: out.headers.location.value };
|
||||
} else {
|
||||
actual = { uri: out.uri };
|
||||
}
|
||||
if (JSON.stringify(actual) === JSON.stringify(expected)) pass += 1;
|
||||
else
|
||||
failures.push(
|
||||
`${uri} -> ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (pass + failures.length !== CASES.length) {
|
||||
throw new Error(`case count ${pass + failures.length} != ${CASES.length}`);
|
||||
}
|
||||
console.log(`router: ${pass} of ${CASES.length} cases pass`);
|
||||
for (const f of failures) console.error(' FAIL ' + f);
|
||||
if (failures.length > 0) process.exit(1);
|
||||
Reference in New Issue
Block a user