feat: SES production access and monitoring; §7 as single source of operational truth

Q19 is closed — SES production access granted in ca-central-1, confirmed in
writing. Nothing now blocks /contact/.

The structural change is the important one. Specs in docs/ carried their own
copies of resource IDs, regions, DNS records and service state. AGENTS.md §7
is now the single source of truth for operational facts and docs/ cite it
rather than restating it, with the rule recorded in CLAUDE.md under
Conventions.

The reason is the previous commit's DKIM inversion, generalised: the same
fact lived in §7 and docs/05, a correction reached one of them, and the stale
copy told an operator to delete the records that authenticate outbound mail.
A duplicated fact is one that will eventually be wrong in one place, and the
copy that goes stale is the one nobody re-reads. Verified by grep over
docs/*.md — no operational identifier remains.

Also in this change:

- §7 records the SES monitoring: SNS topic ses-alerts, alarms
  SES-BounceRate-High (>= 0.03) and SES-ComplaintRate-High (>= 0.001), and
  the deliberate choice of email feedback forwarding over an SNS feedback
  topic at this volume. The ses-alerts email subscription is stamped PENDING
  CONFIRMATION — the alarms currently notify nobody, now tracked as R9 and on
  the cutover checklist.
- docs/05 records why those alarms are a real control: SES suspends above
  roughly a 5% bounce rate, and under 100 messages a month five bounces
  crosses it.
- Q29: the deploy guard now covers AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  (emptiness only, never echoed) and INTAKE_ENDPOINT, promoted to job-level
  env. An empty intake endpoint ships a live form posting to nothing, which
  is worse than a failed build. Executed under sh -e across four input
  states; fails closed, leaks nothing.
- docs/06: account ID removed from the backup-bucket callout, pointing at §10
  instead, as README already does.
- astro.config.mjs: prefetch removed entirely. Any setting ships Astro's
  prefetch script to every page against the zero-JS convention. Recorded as a
  decision; revisit against real Lighthouse numbers.

AGENTS.md entry (r) records the full reasoning.

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:47:17 -04:00
co-authored by Claude Opus 5
parent 6bf1167624
commit 2b6176e4d7
6 changed files with 244 additions and 70 deletions
+24 -22
View File
@@ -121,28 +121,30 @@ permission), or grant `s3:AbortMultipartUpload` on `BUCKET_NAME/*`.
**Repository → Settings → Actions → Variables** — not secrets. These are not
sensitive, and keeping them as variables means they appear in run logs where
they are useful for debugging. Values captured 2026-08-26 by
`scripts/aws-discover.sh`:
they are useful for debugging.
| Variable | Value |
|---|---|
| `AWS_REGION` | `ca-central-1` |
| `S3_BUCKET` | `adr-smlcompany-site` |
| `CLOUDFRONT_DISTRIBUTION_ID` | `E1OK7G98KNKUTA` |
| `INTAKE_ENDPOINT` | `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` |
| `AWS_REGION` | `AGENTS.md` §7 — Region |
| `S3_BUCKET` | §7 — S3 bucket |
| `CLOUDFRONT_DISTRIBUTION_ID` | §7 — CloudFront |
| `INTAKE_ENDPOINT` | §7 — Intake API |
| `BOOKING_URL` | *(empty — parked, R6)* |
IAM policy substitutions: `BUCKET_NAME` = `adr-smlcompany-site`,
`ACCOUNT_ID` = `327082975128`, `DISTRIBUTION_ID` = `E1OK7G98KNKUTA`.
The same four values fill the IAM policy's `BUCKET_NAME`, `ACCOUNT_ID` and
`DISTRIBUTION_ID` placeholders. **They are deliberately not restated here**
§7 is the single source of truth for operational facts, and the copy that goes
stale is always the one nobody re-reads. `scripts/aws-discover.sh` regenerates
them from AWS if §7 ever needs re-verifying.
> **Read this before creating the key.** Account `327082975128` is shared across
> `meshkinilaw.ca`, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and
> `mlp-clientdb-prod-backups` — which **by its name** holds another business's
> production client-database backups; the contents were never inspected, only
> the bucket name observed. A static deploy key for a marketing site lives in
> the same account. The scoped policy is
> what keeps a compromised Gitea runner from reaching any of that. Do not widen
> it, and never put the `user/pouya` credentials in CI.
> **Read this before creating the key.** The AWS account is **not** a
> single-project account: it is shared with several unrelated sites and with a
> bucket whose name indicates another business's production client-database
> backups. `AGENTS.md` §10 has the specifics and the account identifier; they
> are kept there rather than repeated here. A static deploy key for a marketing
> site lives in that same account, and the scoped policy is what keeps a
> compromised Gitea runner from reaching any of it. Do not widen it, and never
> put the `user/pouya` credentials in CI.
### A runner must exist
@@ -204,7 +206,7 @@ The output contains resource names and IDs but no secrets.
> **Do not execute this section.** It describes the design that was rejected
> because Gitea cannot support it. The live procedure is *Create the user* above.
> Nothing here should be created in AWS. Following it would add an unused GitHub
> federation trust to account `327082975128`.
> federation trust to the shared AWS account (`AGENTS.md` §10).
A static `AWS_ACCESS_KEY_ID` never expires, is invisible once set, and grants its
permissions to anyone who can reach the repository. OIDC issues a short-lived
@@ -292,8 +294,8 @@ Tag every production deploy `v<year>.<n>` so a rollback has something to name.
1. Re-run the workflow at the last good tag, or
2. `git revert` and push, or
3. Restore from S3 object versioning — **already Enabled** on
`adr-smlcompany-site` `[verified 2026-08-26 — AGENTS.md §7]`. It is the
3. Restore from S3 object versioning — **already Enabled** on the site bucket
(`AGENTS.md` §7). It is the
difference between a rollback and a rebuild; do not turn it off.
Then invalidate `/*`.
@@ -318,8 +320,8 @@ Then invalidate `/*`.
- [ ] 404 returns a 404 status
- [ ] Security headers present (`securityheaders.com` A or better)
- [ ] **SES identities verified for sending** — confirmed 2026-08-26, re-check at cutover: `aws sesv2 get-email-identity --email-identity smlcompany.ca` and confirm `VerifiedForSendingStatus: true`
- [ ] **SES out of the sandbox** (Q19) — `aws sesv2 get-account --query 'ProductionAccessEnabled'`. In sandbox, mail reaches only pre-verified addresses and the inquirer's confirmation silently fails
- [ ] Intake form tested end to end: DynamoDB record written to `adr-intake-submissions`, both emails delivered to a real inbox, TTL set
- [ ] **SES bounce/complaint alarms actually notify someone**`AGENTS.md` §7 records the `ses-alerts` email subscription as **pending confirmation**, and an unconfirmed SNS subscription drops every message. Confirm it, then `aws sns list-subscriptions-by-topic` and check the ARN is not `PendingConfirmation`. *(SES production access itself is granted — Q19 closed.)*
- [ ] Intake form tested end to end: DynamoDB record written to the intake table (`AGENTS.md` §7), both emails delivered to a real inbox, TTL set
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `BOOKING_URL` is empty, so this passes vacuously until a tool is chosen
- [ ] Favicon set complete
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
@@ -330,7 +332,7 @@ Then invalidate `/*`.
- [ ] Bucket not publicly readable; OAC in force
- [ ] ACM certificate valid; Namecheap validation CNAME still present
- [ ] CloudWatch alarms: Lambda errors, DLQ depth, 5xx rate
- [ ] Billing budget/alarm still active — `aws budgets describe-budgets --account-id 327082975128`. `docs/reference/AWS-Hosting-Guide.md` set up an **AWS Budget**, which `cloudwatch describe-alarms` will never return. Whether one was actually created is not recorded anywhere: confirm, do not assume
- [ ] Billing budget/alarm still active — `aws budgets describe-budgets --account-id "$(aws sts get-caller-identity --query Account --output text)"`. `docs/reference/AWS-Hosting-Guide.md` set up an **AWS Budget**, which `cloudwatch describe-alarms` will never return. Whether one was actually created is not recorded anywhere: confirm, do not assume
**Post-cutover, same day**
- [ ] Sitemap submitted to Google Search Console and Bing Webmaster Tools