Build and deploy / build-and-deploy (push) Failing after 4s
Pouya's rulings of 2026-09-03 (the last two D20 findings) and 2026-09-04 (the spam observation and four mitigations), in one change set. D20 finding 10 — med-arb is billed BY PHASE, each phase at the rates already published, so /fees/'s "Every figure is on this page" is true as written rather than narrowed. FEES.medArb is the single source; docs/07 §Med-arb carries the rule INTERIM against R5, and R5 now carries it back, because a derived price moves silently when a rate moves. D20 finding 13 — conduct undertaking (g), attested 2026-09-03, published as his wording verbatim on /legal/privacy/ and /contact/. The clause that raised the finding promised to DISCLOSE a conflicts check's outcome, which the attestation does not cover; it is struck. D20 now partitions 17 fixed / 2 refuted / 1 owed. Spam, 2026-09-04 — recorded in docs/05 §Observed abuse with the date and signature. A second honeypot (a decoy checkbox, own class, `hidden`, a label that tells a human not to tick it) and scoring that LABELS and never rejects: nothing is dropped, nothing new is stored, and only the operator notification changes. Q65 opens the WAF cost call. The timing floor could not be built: there is no timing check and never has been. docs/05 carries it struck, and every mechanism that would give a real per-visitor clock breaks zero-JS, handler-and-form-only, or D1. Q66. configure.mjs gains section 5 — a custom origin request policy forwarding CloudFront-Viewer-Address on /api/*. Written, dry-run against the live distribution, NOT applied. It reads the handler's own header reads and refuses to run if the whitelist omits one. And reading the live account to do it found four AGENTS.md §7 rows saying the intake backend was undeployed, two days after it went live — corrected against get-function-configuration, get-routes, get-stage, get-policy and the deployed zip, which was downloaded and read. Review: adversarial-reviewer only (claims-auditor is D20's cutover pass and has run). Round 1 five lenses, 56 findings, 7 blocking, 4 refuted by an independent refuter; round 2 four lenses, 36 findings, 33 of them defects in round 1's own repairs. Stopped at two per D19. Gates, exit status read for each: check 0 · build 0 (23 pages) · check:claims 0 · check:intake 0 · og:proof 0 · lint 0 · spam-score.test 39/39 with 6/6 mutations killed · router.test 30/30 · minifier grep 1 (clean) · lighthouse 0, no category below 95 · configure.mjs dry run 0, nothing written. Nothing deployed and nothing applied. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
207 lines
8.5 KiB
JavaScript
207 lines
8.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Cross-checks the intake form's two field tables. `npm run check:intake`.
|
|
*
|
|
* WHY THERE ARE TWO TABLES AT ALL, because the obvious reaction to this script
|
|
* is to delete one of them and share an import. `docs/05-backend-spec.md`:
|
|
* *"Client-side validation is a convenience. **The Lambda re-validates
|
|
* everything.**"* A server that validates against a list the client shipped it
|
|
* is not validating — it is asking the caller what the rules are. And the Lambda
|
|
* is a separately deployed zip that cannot import from `src/` anyway.
|
|
*
|
|
* So the duplication is architectural, and what makes it safe is this check
|
|
* rather than a shared module: the two tables must agree on every field NAME, on
|
|
* which fields are REQUIRED, on every length CAP, and on every closed OPTION
|
|
* SET. If they disagree, the form offers something the handler rejects, or the
|
|
* handler accepts something the form never shows — and the first is a lost
|
|
* inquiry that looks like a bug in the browser.
|
|
*
|
|
* This is the one place in the repo where a duplicated fact is deliberate, and
|
|
* `AGENTS.md`'s standing rule about duplicated facts is why it needs a mechanism
|
|
* on top of a comment.
|
|
*
|
|
* Both files are read directly — Node strips the types out of the `.ts` — so
|
|
* this script holds no third copy of the list.
|
|
*/
|
|
import {
|
|
INTAKE_FIELDS,
|
|
HONEYPOT_FIELD,
|
|
DECOY_CHECKBOX_FIELD,
|
|
} from '../src/data/intake.ts';
|
|
import {
|
|
FIELDS as SERVER_FIELDS,
|
|
HONEYPOT,
|
|
DECOY_CHECKBOX,
|
|
} from '../backend/intake/fields.mjs';
|
|
|
|
/**
|
|
* BOTH TABLES ARE IMPORTED, NOT PARSED. The first version of this script read
|
|
* `handler.mjs` as text, sliced out the `const FIELDS = [ … ]` literal, munged
|
|
* quotes and commas into JSON, and guarded the result with a regex meant to
|
|
* refuse anything executable.
|
|
*
|
|
* **That guard rejected the table on the word `process`, which is a FIELD NAME.**
|
|
* A guard that fires on the data it exists to protect is worse than no guard,
|
|
* and the munging underneath it would have broken on the first apostrophe or
|
|
* URL in a label. The fix was not a better regex: the server's table moved into
|
|
* `backend/intake/fields.mjs`, which has no module-scope side effects and can
|
|
* simply be imported. The independence that matters is that the SERVER's table
|
|
* lives with the server and the handler trusts nothing from `src/` — not that a
|
|
* check script refuses to load it.
|
|
*/
|
|
|
|
const problems = [];
|
|
const server = SERVER_FIELDS;
|
|
|
|
const clientNames = INTAKE_FIELDS.map((f) => f.name);
|
|
const serverNames = server.map((f) => f.name);
|
|
|
|
for (const name of clientNames) {
|
|
if (!serverNames.includes(name)) {
|
|
problems.push(
|
|
`"${name}" is on the form but the handler does not accept it — the ` +
|
|
'inquirer would fill it and it would be silently dropped.',
|
|
);
|
|
}
|
|
}
|
|
for (const name of serverNames) {
|
|
if (!clientNames.includes(name)) {
|
|
problems.push(
|
|
`"${name}" is validated by the handler but is not on the form.`,
|
|
);
|
|
}
|
|
}
|
|
if (HONEYPOT !== HONEYPOT_FIELD) {
|
|
problems.push(
|
|
`honeypot name differs: form "${HONEYPOT_FIELD}", handler "${HONEYPOT}". ` +
|
|
'A bot fills the field the form renders; the handler checks the one it ' +
|
|
'knows about, so a mismatch disables the honeypot silently.',
|
|
);
|
|
}
|
|
if (serverNames.includes(HONEYPOT_FIELD)) {
|
|
problems.push(
|
|
`the honeypot "${HONEYPOT_FIELD}" is in the handler's FIELDS table; it must ` +
|
|
'be checked separately, or a bot filling it would just fail validation ' +
|
|
'instead of being sent to the success page.',
|
|
);
|
|
}
|
|
|
|
/* THE SECOND HONEYPOT GETS THE SAME THREE CHECKS, and it needs a fourth.
|
|
Added 2026-09-04 with the decoy checkbox. Every failure mode below is silent
|
|
in production: a mismatched name disables the trap, a name inside `FIELDS`
|
|
turns it into ordinary validation, and two traps sharing one name is one
|
|
trap with a comment claiming there are two. */
|
|
if (DECOY_CHECKBOX !== DECOY_CHECKBOX_FIELD) {
|
|
problems.push(
|
|
`decoy checkbox name differs: form "${DECOY_CHECKBOX_FIELD}", handler ` +
|
|
`"${DECOY_CHECKBOX}". The form renders one name and the handler checks ` +
|
|
'another, so the trap is disabled and nothing fails.',
|
|
);
|
|
}
|
|
if (serverNames.includes(DECOY_CHECKBOX_FIELD)) {
|
|
problems.push(
|
|
`the decoy checkbox "${DECOY_CHECKBOX_FIELD}" is in the handler's FIELDS ` +
|
|
'table; it must be checked separately, or ticking it would fail ' +
|
|
'validation instead of sending the bot to the success page.',
|
|
);
|
|
}
|
|
if (clientNames.includes(DECOY_CHECKBOX_FIELD)) {
|
|
problems.push(
|
|
`the decoy checkbox "${DECOY_CHECKBOX_FIELD}" is in the form's ` +
|
|
'INTAKE_FIELDS table; it would render as a real, visible field.',
|
|
);
|
|
}
|
|
/* `consent` is submitted by the form and read by the handler, and it is in
|
|
NEITHER field table — so the two checks above cannot see a collision with it.
|
|
A honeypot named `consent` would discard every valid submission behind the
|
|
success page, which is the worst failure this file can fail to catch. */
|
|
for (const [what, name] of [
|
|
['honeypot', HONEYPOT_FIELD],
|
|
['decoy checkbox', DECOY_CHECKBOX_FIELD],
|
|
]) {
|
|
if (name === 'consent') {
|
|
problems.push(
|
|
`the ${what} is named "consent", which the form submits and the handler ` +
|
|
'requires — every valid submission would be discarded behind the ' +
|
|
'success page.',
|
|
);
|
|
}
|
|
}
|
|
if (DECOY_CHECKBOX_FIELD === HONEYPOT_FIELD) {
|
|
problems.push(
|
|
'the two honeypots share the name ' +
|
|
`"${HONEYPOT_FIELD}" — that is one trap, not two, and the second ` +
|
|
'mechanism (a checkbox that must arrive absent) would not exist.',
|
|
);
|
|
}
|
|
/* And the first honeypot must not appear on the form's own table either — the
|
|
mirror of the check above it, which existed only for the handler's side. */
|
|
if (clientNames.includes(HONEYPOT_FIELD)) {
|
|
problems.push(
|
|
`the honeypot "${HONEYPOT_FIELD}" is in the form's INTAKE_FIELDS table; ` +
|
|
'it would render as a real, visible field.',
|
|
);
|
|
}
|
|
|
|
for (const clientField of INTAKE_FIELDS) {
|
|
const serverField = server.find((f) => f.name === clientField.name);
|
|
if (!serverField) continue;
|
|
|
|
if (Boolean(clientField.required) !== Boolean(serverField.required)) {
|
|
problems.push(
|
|
`"${clientField.name}": form required=${Boolean(clientField.required)}, ` +
|
|
`handler required=${Boolean(serverField.required)}. A field the form ` +
|
|
'marks optional and the handler requires is a rejection the inquirer ' +
|
|
'cannot see the reason for.',
|
|
);
|
|
}
|
|
|
|
/* LABELS TOO, since 2026-08-31. The handler now renders `f.label` into the
|
|
confirmation email the inquirer keeps, so a label that drifts from the
|
|
form's own wording means the receipt describes fields by names the form
|
|
never showed. One more comparison; the duplication stays mechanical. */
|
|
if (clientField.label !== serverField.label) {
|
|
problems.push(
|
|
`"${clientField.name}": labels differ.\n` +
|
|
` form: ${JSON.stringify(clientField.label)}\n` +
|
|
` handler: ${JSON.stringify(serverField.label ?? null)}\n` +
|
|
' The handler renders its label into the confirmation email.',
|
|
);
|
|
}
|
|
|
|
if ((clientField.max ?? null) !== (serverField.max ?? null)) {
|
|
problems.push(
|
|
`"${clientField.name}": form max=${clientField.max ?? 'none'}, ` +
|
|
`handler max=${serverField.max ?? 'none'}. The form's maxlength stops ` +
|
|
'typing; a lower cap in the handler rejects a submission that looked fine.',
|
|
);
|
|
}
|
|
|
|
const clientOptions = clientField.options ? [...clientField.options] : null;
|
|
const serverOptions = serverField.options ? [...serverField.options] : null;
|
|
if (JSON.stringify(clientOptions) !== JSON.stringify(serverOptions)) {
|
|
problems.push(
|
|
`"${clientField.name}": option sets differ.\n` +
|
|
` form: ${JSON.stringify(clientOptions)}\n` +
|
|
` handler: ${JSON.stringify(serverOptions)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`check:intake — ${clientNames.length} form fields, ${serverNames.length} ` +
|
|
'handler fields, compared on name, label, requiredness, cap and option ' +
|
|
`set; 2 honeypots ("${HONEYPOT_FIELD}", "${DECOY_CHECKBOX_FIELD}") ` +
|
|
'compared on name and checked out of both tables.',
|
|
);
|
|
if (problems.length > 0) {
|
|
console.error(`\nINTAKE TABLE MISMATCH — ${problems.length}:`);
|
|
for (const p of problems) console.error(` - ${p}`);
|
|
console.error(
|
|
'\ndocs/05: the handler re-validates everything. The two tables are ' +
|
|
'independent on purpose; they still have to agree.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
console.log('OK — the form and the handler agree.');
|