fix: sweep D3 amendment through the specs; correct inverted DKIM table

The re-audit of the deploy-guard change surfaced defects well outside the
diff, including one that would have broken production mail.

docs/05-backend-spec.md had the two SES DKIM sets exactly inverted, labelling
the three records that resolve as "orphans" and the three NXDOMAIN records as
"Live. Never delete". Entry (j) corrected this in AGENTS.md §7 and the
correction never reached docs/05. Since SES has no custom MAIL FROM, DKIM is
the only thing satisfying DMARC, so acting on that table would have silently
broken intake mail authentication.

Also in this change:

- .gitea/workflows/deploy.yml gains a guard as steps[0] that fails the run,
  naming the variable, if AWS_REGION, S3_BUCKET or CLOUDFRONT_DISTRIBUTION_ID
  is empty — how a Gitea too old for the vars context manifests. Verified
  fail-closed under bash -e, sh -e and bash -euo pipefail.
- AGENTS.md Current Truth: SPF and DMARC recorded as present (Q20), the
  matching §10 High risk row retired, three duplicate Q rows removed.
- docs/reference/AWS-Hosting-Guide.md tracked and given a do-not-execute
  banner; it was an executable procedure for the architecture D1/D3 replace.
- Copy decks: "a working litigator" and "an active litigation practice"
  replaced with the register's own wording; LegalService JSON-LD replaced with
  ProfessionalService; tribunal-secretary offers removed per D14; nine stale
  question blockers swept.
- astro.config.mjs: prefetchAll disabled — it injected JS into every page
  against the zero-JS convention with no decision recorded.
- src/data/site.ts: unregistered response-time commitment nulled (Q27);
  OBA section names downgraded to [assumed] (Q28).
- s3:AbortMultipartUpload reasoning corrected to measure ./dist, not the repo.

Opens Q27, Q28, Q29. AGENTS.md entry (q) records the full resolution,
including the findings declined and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
Pouya Lajevardi
2026-08-26 11:28:42 -04:00
co-authored by Claude Opus 5
parent e6abdf42e8
commit 6bf1167624
22 changed files with 1951 additions and 183 deletions
+31 -26
View File
@@ -1,7 +1,9 @@
# 05 — Intake, booking, and data handling
Authority: `AGENTS.md` §3 D10 — rebuilt intake form plus calendar booking.
Existing infrastructure is documented in `AWS-Hosting-Guide.md` Parts 810.
Existing infrastructure is authoritative in `AGENTS.md` §7. How it was built is
recorded in `docs/reference/AWS-Hosting-Guide.md` Parts 810 — a historical
record with a do-not-execute banner, superseded by §7 wherever they disagree.
**Read that guide before changing anything**; the resources already exist and
were built by hand in the console.
@@ -10,7 +12,7 @@ were built by hand in the console.
## What exists today
API Gateway (HTTP API) → Lambda → DynamoDB, with SES for notification email and
a verified sender on `smlcompany.ca`. `[verified 2026-08-25 — AWS-Hosting-Guide.md]`
a verified sender on `smlcompany.ca`. `[verified 2026-08-26 — AGENTS.md §7]`
The shape is right. This is a hardening and rework pass, not a replacement.
@@ -73,7 +75,8 @@ Client-side validation is a convenience. **The Lambda re-validates everything.**
DynamoDB, `ca-central-1` — **Canadian data residency is a real selling point for
a Canadian legal practice, and the privacy policy will say so.** Confirm the
existing table's region and migrate if it is elsewhere (**Q10**).
existing table's region and migrate if it is elsewhere. `AGENTS.md` §7 records
`adr-intake-submissions` in `ca-central-1` `[verified 2026-08-26]`.
| Attribute | |
|---|---|
@@ -106,33 +109,33 @@ SES on submission:
of the no-retainer language, and a link to the privacy policy. This email is
the reason the form beats a `mailto:` link.
**Email authentication — verified 2026-08-26, and it is not in place.**
**Email authentication — in place as of 2026-08-26 (Q20).**
A DNS query of `smlcompany.ca` found **no SPF record and no DMARC record**. Mail
is on Google Workspace (MX `1 smtp.google.com`) with Google DKIM configured, and
the SES domain identity reports verified for sending — but neither SPF nor DMARC
exists.
**What is already in place** (Namecheap DNS and the SES console, both inspected
2026-08-26):
| Record | Status |
|---|---|
| SES DKIM — `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…` `._domainkey` | **Live.** Matches SES exactly. Never delete |
| SES DKIM — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` `._domainkey` | Orphans from an earlier verification. Inert. **Leave them** — deleting the wrong three breaks DKIM |
| `google._domainkey` TXT | Google Workspace DKIM. Never delete |
| Two CNAMEs → `jkddzztszm.acm-validations.aws` | ACM certificate validation. **Never delete** — breaks HTTPS at the next renewal |
| `adr` CNAME → `d26v23dhgsp2ta.cloudfront.net` | The site |
| Custom MAIL FROM | **Not configured.** Optional; would add SPF alignment |
Add both of these; neither conflicts with anything above:
SPF and DMARC were both added by Pouya and independently verified on
2026-08-26. Mail is on Google Workspace (MX `1 smtp.google.com`) with Google
DKIM configured, and the SES domain identity is verified for sending in
`ca-central-1`. **An earlier version of this spec said neither record existed;
that was true when written and is no longer.** The records now live are:
| Host | Type | Value |
|---|---|---|
| `@` | TXT | `v=spf1 include:_spf.google.com include:amazonses.com ~all` |
| `_dmarc` | TXT | `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` |
A domain may publish **only one** `v=spf1` record, so both senders go in one
**What is already in place** (Namecheap DNS and the SES console, both inspected
2026-08-26):
| Record | Status |
|---|---|
| SES DKIM — `f5puwearz…`, `jdue2r22c…`, `kznn3cklv…` `._domainkey` | **LIVE. Never delete.** All three resolve (NOERROR) and back the healthy `ca-central-1` SES identity — `DkimStatus: SUCCESS`. These are the records DMARC alignment rests on `[verified 2026-08-26 — DNS, AGENTS.md §7]` |
| SES DKIM — `3zsnvsjg…`, `jejgp7na3…`, `xpiwyftpo…` `._domainkey` | **BROKEN and inert.** Entered into Namecheap with the full name in the Host field, so the zone doubled the domain; they answer NXDOMAIN at the correct name. They belong to a stray `us-east-1` identity this project does not use. Harmless where they are — leaving them is the low-risk choice (Q21) `[verified 2026-08-26 — DNS, AGENTS.md §7]` |
| `google._domainkey` TXT | Google Workspace DKIM. Never delete |
| Two CNAMEs → `jkddzztszm.acm-validations.aws` | ACM certificate validation. **Never delete** — breaks HTTPS at the next renewal |
| `adr` CNAME → `d26v23dhgsp2ta.cloudfront.net` | The site |
| Custom MAIL FROM | **Not configured.** Optional; would add SPF alignment |
Notes that mattered when these were added, kept because they matter again on
any future edit: a domain may publish **only one** `v=spf1` record, so both senders go in one
string. Namecheap TXT values take **no surrounding quotes** — quoting them stores
the quotes literally and breaks the record.
@@ -140,7 +143,8 @@ the quotes literally and breaks the record.
SES here. Without a custom MAIL FROM domain, SES uses an envelope sender at
`amazonses.com`, so its SPF pass is not *aligned* with `smlcompany.ca` and does
not satisfy DMARC. **SES satisfies DMARC through DKIM alignment** — that is what
the six CNAMEs above are doing, and it already works. The SPF record's real job
the **three resolving** DKIM CNAMEs above are doing, and it already works. (Six
are present in the zone; only the `f5pu` / `jdue` / `kznn` set answers.) The SPF record's real job
is authenticating **Google Workspace** mail, which currently has no SPF at all.
`include:amazonses.com` is harmless and becomes useful if a custom MAIL FROM
domain is configured later.
@@ -152,7 +156,7 @@ no visibility.
**Do not delete the ACM validation CNAMEs.** They are how the certificate for
`adr.smlcompany.ca` auto-renews. Removing them breaks HTTPS at the next renewal
— silently, months later (Q20).
— silently, months later.
Failure handling: SES failure must never lose the submission. Write to DynamoDB
first, then send. A dead-letter queue on the Lambda, and a CloudWatch alarm on
@@ -204,7 +208,8 @@ relationship · cookie and analytics disclosure · last-updated date.
If analytics ship, prefer a cookieless privacy-preserving tool (Plausible,
Fathom). GA4 on a page collecting legal-dispute information is a poor fit for a
practice whose privacy posture is part of its offer (**Q11**).
practice whose privacy posture is part of its offer — D15 settles this:
Plausible or Fathom, cookieless, no consent banner.
## Definition of done