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:
co-authored by
Claude Opus 5
parent
6bf1167624
commit
2b6176e4d7
+26
-14
@@ -37,6 +37,9 @@ jobs:
|
|||||||
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
AWS_DEFAULT_REGION: ${{ vars.AWS_REGION }}
|
||||||
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
S3_BUCKET: ${{ vars.S3_BUCKET }}
|
||||||
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
CLOUDFRONT_DISTRIBUTION_ID: ${{ vars.CLOUDFRONT_DISTRIBUTION_ID }}
|
||||||
|
# Job-level so the guard can see it. An empty INTAKE_ENDPOINT does not
|
||||||
|
# fail the build - it ships a live contact form posting to nothing.
|
||||||
|
INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# Runs first, before checkout and before any AWS call, so a
|
# Runs first, before checkout and before any AWS call, so a
|
||||||
@@ -48,26 +51,33 @@ jobs:
|
|||||||
# to interpolate to an empty string, the sync target below degrades to
|
# to interpolate to an empty string, the sync target below degrades to
|
||||||
# "s3://", and the run dies obscurely somewhere in the middle.
|
# "s3://", and the run dies obscurely somewhere in the middle.
|
||||||
#
|
#
|
||||||
# SCOPE: this guard covers the three DEPLOY-TARGET variables only. It does
|
# Covers the deploy-target variables, the intake endpoint, AND the two
|
||||||
# NOT cover AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (an unset secret
|
# secrets. The secrets matter most: AGENTS.md Q22 records that nobody has
|
||||||
# still fails later, at `aws sts get-caller-identity`), nor
|
# confirmed the IAM user or its key exists, so an unset key is the single
|
||||||
# vars.INTAKE_ENDPOINT, which is step-scoped on the Build step and whose
|
# likeliest first-run failure - and without this it would burn a whole
|
||||||
# absence would ship a form posting to an empty endpoint. See AGENTS.md
|
# build before dying at `aws sts get-caller-identity`.
|
||||||
# Q23 - extending the guard to both is an open decision, not an oversight.
|
#
|
||||||
- name: Guard - required repository variables are set
|
# Only emptiness is ever tested. No value is echoed, so nothing here can
|
||||||
|
# leak a secret into the run log.
|
||||||
|
- name: Guard - required variables and secrets are set
|
||||||
run: |
|
run: |
|
||||||
missing=''
|
missing=''
|
||||||
[ -n "$AWS_DEFAULT_REGION" ] || missing="$missing AWS_REGION"
|
[ -n "$AWS_DEFAULT_REGION" ] || missing="$missing AWS_REGION(var)"
|
||||||
[ -n "$S3_BUCKET" ] || missing="$missing S3_BUCKET"
|
[ -n "$S3_BUCKET" ] || missing="$missing S3_BUCKET(var)"
|
||||||
[ -n "$CLOUDFRONT_DISTRIBUTION_ID" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID"
|
[ -n "$CLOUDFRONT_DISTRIBUTION_ID" ] || missing="$missing CLOUDFRONT_DISTRIBUTION_ID(var)"
|
||||||
|
[ -n "$INTAKE_ENDPOINT" ] || missing="$missing INTAKE_ENDPOINT(var)"
|
||||||
|
[ -n "$AWS_ACCESS_KEY_ID" ] || missing="$missing AWS_ACCESS_KEY_ID(secret)"
|
||||||
|
[ -n "$AWS_SECRET_ACCESS_KEY" ] || missing="$missing AWS_SECRET_ACCESS_KEY(secret)"
|
||||||
if [ -n "$missing" ]; then
|
if [ -n "$missing" ]; then
|
||||||
echo "Missing repository variables:$missing"
|
echo "Not set:$missing"
|
||||||
echo
|
echo
|
||||||
echo 'Set them at Settings -> Actions -> Variables (see docs/06-deployment.md).'
|
echo 'Variables: Settings -> Actions -> Variables.'
|
||||||
echo 'If they ARE set, this Gitea instance predates the vars context (1.21+).'
|
echo 'Secrets: Settings -> Actions -> Secrets.'
|
||||||
|
echo 'See docs/06-deployment.md.'
|
||||||
|
echo 'If the variables ARE set, this Gitea predates the vars context (1.21+).'
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo 'Required repository variables are present.'
|
echo 'All required variables and secrets are set.'
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -86,6 +96,8 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
env:
|
env:
|
||||||
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
PUBLIC_SITE_URL: https://adr.smlcompany.ca
|
||||||
|
# vars, not env — Gitea expression-context support is the very thing
|
||||||
|
# the guard above exists to not depend on.
|
||||||
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
PUBLIC_INTAKE_ENDPOINT: ${{ vars.INTAKE_ENDPOINT }}
|
||||||
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
PUBLIC_BOOKING_URL: ${{ vars.BOOKING_URL }}
|
||||||
|
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
|
|||||||
| Intake Lambda | `adr-intake-handler`, `nodejs24.x` `[verified 2026-08-26]` |
|
| Intake Lambda | `adr-intake-handler`, `nodejs24.x` `[verified 2026-08-26]` |
|
||||||
| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]` |
|
| Intake table | `adr-intake-submissions` (DynamoDB, ca-central-1) `[verified 2026-08-26]` |
|
||||||
| SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` |
|
| SES identities | Domain `smlcompany.ca` **verified for sending** `[verified 2026-08-26]`; addresses `info@`, `intake@`, `adr@` |
|
||||||
| SES account | **In the sandbox** — `ProductionAccessEnabled: false` `[verified 2026-08-26]`. Production access required (Q19) |
|
| SES account | **Production access GRANTED** — out of the sandbox in `ca-central-1`, confirmed by AWS in writing and effective immediately `[verified 2026-08-26 — Q19 closed]`. Mail now reaches unverified recipients, so the inquirer confirmation in D18 works |
|
||||||
| Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` |
|
| Mail hosting | **Google Workspace** — MX `1 smtp.google.com`; `google._domainkey` present, so Google DKIM is configured `[verified 2026-08-26 — DNS query]` |
|
||||||
| **SPF** | **PRESENT.** `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]` |
|
| **SPF** | **PRESENT.** `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]` |
|
||||||
| **DMARC** | **PRESENT.** `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]`. Still at `p=none`; tighten to `quarantine` only after reports come back clean |
|
| **DMARC** | **PRESENT.** `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`, added by Pouya and independently verified `[verified 2026-08-26 — DNS query, Q20]`. Still at `p=none`; tighten to `quarantine` only after reports come back clean |
|
||||||
@@ -258,8 +258,11 @@ the audience it targets. Revisit at month 12–18. `[verified 2026-08-25 — dec
|
|||||||
| SES DKIM — **the set that is BROKEN** | `3zsnvsjg3ddi6hjyndjy6jgjoitvhydl`, `jejgp7na3wdpprsanamct4uxxgw2tyqw`, `xpiwyftpodmgpnzfof3ee7t7fzmlmhnh` — listed in the SES console, present in Namecheap, but **NXDOMAIN**. They were entered with the full name in Namecheap's Host field, so the zone doubled the domain: `<token>._domainkey.smlcompany.ca.smlcompany.ca` resolves, the correct name does not `[verified 2026-08-26 — DNS]` |
|
| SES DKIM — **the set that is BROKEN** | `3zsnvsjg3ddi6hjyndjy6jgjoitvhydl`, `jejgp7na3wdpprsanamct4uxxgw2tyqw`, `xpiwyftpodmgpnzfof3ee7t7fzmlmhnh` — listed in the SES console, present in Namecheap, but **NXDOMAIN**. They were entered with the full name in Namecheap's Host field, so the zone doubled the domain: `<token>._domainkey.smlcompany.ca.smlcompany.ca` resolves, the correct name does not `[verified 2026-08-26 — DNS]` |
|
||||||
| SES identity — **`ca-central-1` (the one that matters)** | `Verified: true`, `DkimStatus: SUCCESS`, tokens `kznn…` / `jdue…` / `f5pu…` — the set that resolves. **Healthy. Change nothing** `[verified 2026-08-26]` |
|
| SES identity — **`ca-central-1` (the one that matters)** | `Verified: true`, `DkimStatus: SUCCESS`, tokens `kznn…` / `jdue…` / `f5pu…` — the set that resolves. **Healthy. Change nothing** `[verified 2026-08-26]` |
|
||||||
| SES identity — `us-east-1` (unused) | `Verified: false`, `DkimStatus: PENDING`, tokens `3zsn…` / `jejgp…` / `xpiwy…` — the doubled-domain set. A stray identity in a region this project does not use `[verified 2026-08-26]` |
|
| SES identity — `us-east-1` (unused) | `Verified: false`, `DkimStatus: PENDING`, tokens `3zsn…` / `jejgp…` / `xpiwy…` — the doubled-domain set. A stray identity in a region this project does not use `[verified 2026-08-26]` |
|
||||||
| ⚠️ **SES sandbox is per-region** | Production access must be requested **in `ca-central-1`**. The AWS console defaults to `us-east-1`; requesting there grants access to the wrong region and the intake email still fails `[verified 2026-08-26]` |
|
| ⚠️ **SES sandbox is per-region** | Recorded as a standing caution, not an open task — production access **was** granted in `ca-central-1` (Q19). The trap: the AWS console defaults to `us-east-1`, and granting access there leaves `ca-central-1` in the sandbox with the intake email still failing. Applies again to any future SES request `[verified 2026-08-26]` |
|
||||||
| SES custom MAIL FROM | **Not configured** — SES reports "No MAIL FROM records found". This is why SES's SPF is unaligned and DMARC rests on DKIM. Optional future improvement, not required `[verified 2026-08-26]` |
|
| SES custom MAIL FROM | **Not configured** — SES reports "No MAIL FROM records found". This is why SES's SPF is unaligned and DMARC rests on DKIM. Optional future improvement, not required `[verified 2026-08-26]` |
|
||||||
|
| SES monitoring — SNS topic | **`ses-alerts`**, `ca-central-1`. Email subscription to `info@smlcompany.ca` is **PENDING CONFIRMATION, not confirmed** `[verified 2026-08-26]`. **Until the subscription is confirmed the alarms below fire into nothing** — confirm the link in the subscription email |
|
||||||
|
| SES monitoring — alarms | **`SES-BounceRate-High`** (bounce rate ≥ `0.03`) and **`SES-ComplaintRate-High`** (complaint rate ≥ `0.001`), both `ca-central-1`, `treat-missing-data: notBreaching` `[verified 2026-08-26]` |
|
||||||
|
| SES bounce/complaint handling | **Email feedback forwarding** — SES's default, on unless disabled. **Deliberately not an SNS feedback topic:** at under 100 messages a month there is nothing to consume a programmatic feed. Revisit when code needs to act on a bounce `[verified 2026-08-26]` |
|
||||||
| **DMARC alignment** | SES satisfies DMARC via **DKIM**, not SPF — without a custom MAIL FROM domain its envelope sender is at `amazonses.com` and is unaligned. The SPF record's real job is authenticating **Google Workspace** mail `[verified 2026-08-26]` |
|
| **DMARC alignment** | SES satisfies DMARC via **DKIM**, not SPF — without a custom MAIL FROM domain its envelope sender is at `amazonses.com` and is unaligned. The SPF record's real job is authenticating **Google Workspace** mail `[verified 2026-08-26]` |
|
||||||
| **Do not delete** | Two ACM validation CNAMEs → `jkddzztszm.acm-validations.aws`. They renew the `adr.smlcompany.ca` certificate; deleting them breaks HTTPS silently at the next renewal `[verified 2026-08-26]` |
|
| **Do not delete** | Two ACM validation CNAMEs → `jkddzztszm.acm-validations.aws`. They renew the `adr.smlcompany.ca` certificate; deleting them breaks HTTPS silently at the next renewal `[verified 2026-08-26]` |
|
||||||
| Root domain | `@` carries a Namecheap **URL Redirect Record** to `sml-coming-soon.ca`. TXT records coexist with it — adding SPF does not disturb it `[verified 2026-08-26]` |
|
| Root domain | `@` carries a Namecheap **URL Redirect Record** to `sml-coming-soon.ca`. TXT records coexist with it — adding SPF does not disturb it `[verified 2026-08-26]` |
|
||||||
@@ -309,14 +312,14 @@ Nothing below can be invented. Each needs an answer from Pouya.
|
|||||||
| ~~Q9~~ | **ANSWERED 2026-08-26.** Gitea, repo `adr-sml`, clone at `/Users/pouya/Dev/Websites/adr-sml` | — |
|
| ~~Q9~~ | **ANSWERED 2026-08-26.** Gitea, repo `adr-sml`, clone at `/Users/pouya/Dev/Websites/adr-sml` | — |
|
||||||
| ~~Q10~~ | **ANSWERED 2026-08-26.** Full inventory captured; values in §7 | — |
|
| ~~Q10~~ | **ANSWERED 2026-08-26.** Full inventory captured; values in §7 | — |
|
||||||
| ~~Q18~~ | **ANSWERED 2026-08-26.** `smlcompany.ca` is verified for sending — `true`. The `None` in the earlier listing was a query artefact, not a fault | — |
|
| ~~Q18~~ | **ANSWERED 2026-08-26.** `smlcompany.ca` is verified for sending — `true`. The `None` in the earlier listing was a query artefact, not a fault | — |
|
||||||
| **Q19** | **CONFIRMED AS A BLOCKER 2026-08-26. Request it in `ca-central-1`, not the console's default `us-east-1` — the sandbox is per-region.** `ProductionAccessEnabled: false` — the SES account is in the **sandbox**, so mail reaches only pre-verified addresses. Confirmed with Pouya (D18) that the inquirer receipt is wanted, so production access **is** required. ~24 h AWS turnaround. **The only item on this project with external lead time** | The confirmation half of `/contact/` |
|
| ~~Q19~~ | **ANSWERED 2026-08-26. Production access granted** in `ca-central-1`, confirmed by AWS in writing, effective immediately. The per-region trap was real and was avoided — it was requested in `ca-central-1`, not the console's default `us-east-1`. **Nothing now blocks `/contact/`** | — |
|
||||||
| ~~Q20~~ | **ANSWERED 2026-08-26.** Both records added by Pouya and independently verified: SPF `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, DMARC `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`. Both existing `@` TXT records, Google DKIM, MX, and the `adr` CNAME all confirmed intact | — |
|
| ~~Q20~~ | **ANSWERED 2026-08-26.** Both records added by Pouya and independently verified: SPF `v=spf1 include:_spf.google.com include:amazonses.com ~all` on `@`, DMARC `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` on `_dmarc`. Both existing `@` TXT records, Google DKIM, MX, and the `adr` CNAME all confirmed intact | — |
|
||||||
| ~~Q21~~ | **ANSWERED 2026-08-26.** `ca-central-1` is healthy (`SUCCESS`) on the resolving token set. The broken records belong to a stray `us-east-1` identity this project does not use. **Nothing to fix.** The three dead CNAMEs are inert; leaving them is the low-risk choice | — |
|
| ~~Q21~~ | **ANSWERED 2026-08-26.** `ca-central-1` is healthy (`SUCCESS`) on the resolving token set. The broken records belong to a stray `us-east-1` identity this project does not use. **Nothing to fix.** The three dead CNAMEs are inert; leaving them is the low-risk choice | — |
|
||||||
| ~~Q11~~ | **ANSWERED 2026-08-26.** Privacy-first, cookieless — Plausible or Fathom. No GA4, no consent banner | — |
|
| ~~Q11~~ | **ANSWERED 2026-08-26.** Privacy-first, cookieless — Plausible or Fathom. No GA4, no consent banner | — |
|
||||||
| ~~Q12~~ | **ANSWERED 2026-08-26.** `https://www.linkedin.com/in/pouyalajevardi/` | — |
|
| ~~Q12~~ | **ANSWERED 2026-08-26.** `https://www.linkedin.com/in/pouyalajevardi/` | — |
|
||||||
| **Q27** | **Is a one-business-day response time a commitment you want published?** `src/data/site.ts` carried `responseTime: 'Inquiries are answered within one business day.'` — a service level a reader can hold you to, and not in the §4 register. Set to `null` with a `TODO(pouya)` until confirmed. Raised by `claims-auditor`, 2026-08-26 | The `/contact/` page copy |
|
| **Q27** | **Is a one-business-day response time a commitment you want published?** `src/data/site.ts` carried `responseTime: 'Inquiries are answered within one business day.'` — a service level a reader can hold you to, and not in the §4 register. Set to `null` with a `TODO(pouya)` until confirmed. Raised by `claims-auditor`, 2026-08-26 | The `/contact/` page copy |
|
||||||
| **Q28** | **Which OBA sections?** §4 verifies OBA membership but marks the *section list* `[assumed]` — the brief names Construction & Infrastructure, ADR, and Civil Litigation. `src/data/site.ts` was publishing all three under a blanket `[verified]` stamp; now marked and flagged. Raised by `claims-auditor`, 2026-08-26 | The `/about/` credentials list |
|
| **Q28** | **Which OBA sections?** §4 verifies OBA membership but marks the *section list* `[assumed]` — the brief names Construction & Infrastructure, ADR, and Civil Litigation. `src/data/site.ts` was publishing all three under a blanket `[verified]` stamp; now marked and flagged. Raised by `claims-auditor`, 2026-08-26 | The `/about/` credentials list |
|
||||||
| **Q29** | **Should the deploy guard cover the secrets and `INTAKE_ENDPOINT` too?** As built it covers the three deploy-target variables only. Two gaps, both real: an unset `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` — **the most likely first-run failure, since Q22 is unanswered** — passes the guard and burns a full build before dying at `sts get-caller-identity`; and an empty `vars.INTAKE_ENDPOINT` passes silently and **ships a live form posting to nothing**. The second needs the variable promoted to job-level `env:`. Not done: you specified three variables and this widens the guard. Raised by `adversarial-reviewer`, 2026-08-26 | Nothing yet — `src/pages/` is empty. Blocks `/contact/` shipping safely |
|
| ~~Q29~~ | **ANSWERED 2026-08-26 by implementation.** Guard widened on both counts: the two secrets are checked (emptiness only, never echoed), and `INTAKE_ENDPOINT` is promoted to job-level `env:` and guarded. Every `vars.*` and `secrets.*` the workflow consumes is now covered except `BOOKING_URL`, empty by decision (R6) | — |
|
||||||
| **Q23** | **Is a Gitea `act_runner` registered to this repository or its organisation?** Without one the workflow queues silently and never runs — indistinguishable from a broken pipeline (`docs/06-deployment.md`). *The Gitea-version half of this question is closed:* rather than record a version fact that goes stale, `.gitea/workflows/deploy.yml` now runs a guard as its **first** step that fails the run if `AWS_REGION`, `S3_BUCKET`, or `CLOUDFRONT_DISTRIBUTION_ID` is empty — which is how a pre-1.21 instance manifests. Pouya's decision, 2026-08-26. Runner registration is not something a guard can cover | The first deploy |
|
| **Q23** | **Is a Gitea `act_runner` registered to this repository or its organisation?** Without one the workflow queues silently and never runs — indistinguishable from a broken pipeline (`docs/06-deployment.md`). *The Gitea-version half of this question is closed:* rather than record a version fact that goes stale, `.gitea/workflows/deploy.yml` now runs a guard as its **first** step that fails the run if `AWS_REGION`, `S3_BUCKET`, or `CLOUDFRONT_DISTRIBUTION_ID` is empty — which is how a pre-1.21 instance manifests. Pouya's decision, 2026-08-26. Runner registration is not something a guard can cover | The first deploy |
|
||||||
|
|
||||||
## 10. Risks
|
## 10. Risks
|
||||||
@@ -330,7 +333,7 @@ Nothing below can be invented. Each needs an answer from Pouya.
|
|||||||
| Cutover breaks the live site (D11 is a single-shot deploy) | Medium | Full pre-cutover checklist in `docs/06-deployment.md`; CloudFront can be rolled back to the prior origin path |
|
| Cutover breaks the live site (D11 is a single-shot deploy) | Medium | Full pre-cutover checklist in `docs/06-deployment.md`; CloudFront can be rolled back to the prior origin path |
|
||||||
| Twenty pages of thin copy rank worse than six good ones | Medium | Each page must justify itself with substantive content. Ship fewer pages rather than padded ones |
|
| Twenty pages of thin copy rank worse than six good ones | Medium | Each page must justify itself with substantive content. Ship fewer pages rather than padded ones |
|
||||||
| **Deploy-credential blast radius.** AWS account `327082975128` is not a single-project account. It also holds `meshkinilaw.ca` and its preview site, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and **`mlp-clientdb-prod-backups-327082975128`** (recorded in an earlier entry with the account suffix; the short form `mlp-clientdb-prod-backups` is used elsewhere in the docs) — which **by its name** holds production client-database backups. Only the name was ever observed; the contents were not inspected and the owner is not established | **High** | A static deploy key for a personal website must never be able to reach a client database. The scoped IAM policy in `docs/06-deployment.md` grants four actions on one bucket and one distribution and nothing else — that narrowness is now load-bearing, not hygiene. Never widen it. Never reuse the `user/pouya` credentials in CI `[verified 2026-08-26 — inventory]` |
|
| **Deploy-credential blast radius.** AWS account `327082975128` is not a single-project account. It also holds `meshkinilaw.ca` and its preview site, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and **`mlp-clientdb-prod-backups-327082975128`** (recorded in an earlier entry with the account suffix; the short form `mlp-clientdb-prod-backups` is used elsewhere in the docs) — which **by its name** holds production client-database backups. Only the name was ever observed; the contents were not inspected and the owner is not established | **High** | A static deploy key for a personal website must never be able to reach a client database. The scoped IAM policy in `docs/06-deployment.md` grants four actions on one bucket and one distribution and nothing else — that narrowness is now load-bearing, not hygiene. Never widen it. Never reuse the `user/pouya` credentials in CI `[verified 2026-08-26 — inventory]` |
|
||||||
| **SES is in the sandbox** — confirmed, not hypothetical | **High** | Q19. Until production access is granted, every confirmation email to an inquirer fails silently. The site appears to work; the inquirer concludes they were ignored. Request access early — it is the only dependency with an external turnaround |
|
| ~~SES is in the sandbox~~ | ~~High~~ → **RESOLVED 2026-08-26** | Q19 closed: production access granted in `ca-central-1`, confirmed in writing. Residual, tracked in §7: the `ses-alerts` SNS email subscription is **pending confirmation**, so the bounce and complaint alarms currently notify nobody `[verified 2026-08-26]` |
|
||||||
| ~~No SPF and no DMARC on `smlcompany.ca`~~ | ~~High~~ → **RESOLVED 2026-08-26** | Both records added by Pouya and independently verified (Q20). SPF authenticates Google Workspace mail; SES satisfies DMARC through DKIM alignment on the three resolving CNAMEs. DMARC sits at `p=none` — the residual task is to review reports and tighten to `quarantine`, which is monitoring, not a risk `[verified 2026-08-26 — DNS query]` |
|
| ~~No SPF and no DMARC on `smlcompany.ca`~~ | ~~High~~ → **RESOLVED 2026-08-26** | Both records added by Pouya and independently verified (Q20). SPF authenticates Google Workspace mail; SES satisfies DMARC through DKIM alignment on the three resolving CNAMEs. DMARC sits at `p=none` — the residual task is to review reports and tighten to `quarantine`, which is monitoring, not a risk `[verified 2026-08-26 — DNS query]` |
|
||||||
|
|
||||||
## 11. Glossary
|
## 11. Glossary
|
||||||
@@ -370,6 +373,7 @@ never being raised again.
|
|||||||
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
|
| R4 | **Insights cadence.** D9 commits to monthly. A blog that stops is worse than one that never started | 2026-08-26 | The section's whole value is compounding |
|
||||||
| R5 | **Fee review at 12 months.** Published rates are sticky; the right moment to move them is deliberate, not reactive | 2026-08-26 | D14 is priced for where the practice is going, not where it is |
|
| R5 | **Fee review at 12 months.** Published rates are sticky; the right moment to move them is deliberate, not reactive | 2026-08-26 | D14 is priced for where the practice is going, not where it is |
|
||||||
| R6 | **Booking tool.** Parked by Pouya on 2026-08-26; `/contact/` ships with the intake form and a reserved slot for an embed | 2026-08-26 | He asked to be reminded. D10 committed to booking because it removes the back-and-forth that loses appointments — the form alone is a partial answer |
|
| R6 | **Booking tool.** Parked by Pouya on 2026-08-26; `/contact/` ships with the intake form and a reserved slot for an embed | 2026-08-26 | He asked to be reminded. D10 committed to booking because it removes the back-and-forth that loses appointments — the form alone is a partial answer |
|
||||||
|
| R9 | **The SES alarms notify nobody until the `ses-alerts` email subscription is confirmed.** `SES-BounceRate-High` and `SES-ComplaintRate-High` are configured and live; the SNS email subscription to `info@smlcompany.ca` is **pending confirmation**, and an unconfirmed subscription drops every message | 2026-08-26 | A monitoring control that exists but does not deliver is worse than none, because it reads as covered. At this volume five bounces can cross the ~5% suspension threshold. Tracked in §7 and on the cutover checklist, but a one-click task nobody owns is exactly what §12 is for |
|
||||||
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
|
| ~~R7~~ | **RATIFIED / SUPERSEDED 2026-08-26.** (a) Cache-policy table matching the pipeline — **accepted**; documenting what the pipeline does beats documenting an intention. (b) `s3:AbortMultipartUpload` omitted — **accepted, reasoning corrected**: the lifecycle rule does not exist and is therefore not the cover; the actual cover is that `aws s3 sync` only goes multipart above 8 MB and the largest asset is a 357 KB portrait. Recorded in `docs/06-deployment.md` with a revisit trigger. (c) The `aws s3 ls` pre-flight — **superseded** by the variable guard now running as the workflow's first step | 2026-08-26 | — |
|
||||||
| ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — |
|
| ~~R8~~ | **PROMOTED TO A RULE 2026-08-26.** A reminder was too weak for a pattern that survived three entries. *A sweep is a command, not a claim* now sits in `CLAUDE.md` under Conventions, in `/build` Phase 6, and in `/wrap` step 3: any claim that a change was applied across files must cite the command and be written only after reading its output | 2026-08-26 | — |
|
||||||
|
|
||||||
@@ -377,6 +381,128 @@ never being raised again.
|
|||||||
|
|
||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
|
## 2026-08-26 (r) — Q19 closed; SES monitoring recorded; §7 made the single source of operational truth
|
||||||
|
|
||||||
|
**Who:** Pouya closed Q19, supplied the monitoring configuration, ruled on Q29,
|
||||||
|
and made the structural call below. Claude Code implemented.
|
||||||
|
|
||||||
|
### Q19 — CLOSED. Nothing now blocks `/contact/`
|
||||||
|
|
||||||
|
**Production access granted** in `ca-central-1`, confirmed by AWS in writing,
|
||||||
|
effective immediately. §7's SES account row and §9's Q19 both record it; §10's
|
||||||
|
**High** risk row for the sandbox is struck as resolved.
|
||||||
|
|
||||||
|
The per-region trap is kept in §7 as a **standing caution rather than an open
|
||||||
|
task** — it was avoided, and it applies again to any future SES request. Deleting
|
||||||
|
the row would delete the lesson.
|
||||||
|
|
||||||
|
### Monitoring exists, and one part of it does not work yet
|
||||||
|
|
||||||
|
Recorded in §7 (the facts) and `docs/05-backend-spec.md` (why it matters):
|
||||||
|
|
||||||
|
- **SNS topic `ses-alerts`**, `ca-central-1`. The email subscription to
|
||||||
|
`info@smlcompany.ca` is **PENDING CONFIRMATION**, stamped that way deliberately.
|
||||||
|
An unconfirmed SNS subscription **drops every message**, so as things stand the
|
||||||
|
alarms below fire into nothing. That is now a cutover checklist item in its own
|
||||||
|
right, checked with `sns list-subscriptions-by-topic` rather than assumed.
|
||||||
|
- **`SES-BounceRate-High`** (≥ `0.03`) and **`SES-ComplaintRate-High`**
|
||||||
|
(≥ `0.001`), both `ca-central-1`, `treat-missing-data: notBreaching`.
|
||||||
|
- **Bounce and complaint handling is SES email feedback forwarding** — the
|
||||||
|
default — **not an SNS feedback topic**, deliberately. Under 100 messages a
|
||||||
|
month there is nothing to consume a programmatic feed, and an unused topic is
|
||||||
|
one more thing to keep correct. Revisit when code needs to *act* on a bounce:
|
||||||
|
suppression lists, retry logic, marking a record undeliverable.
|
||||||
|
|
||||||
|
`docs/05` records why these are a real control rather than a formality: **SES
|
||||||
|
suspends sending above roughly a 5% bounce rate, and at this volume five bounces
|
||||||
|
crosses it.** An intake form is exactly where mistyped addresses arrive. The
|
||||||
|
alarms sit well below that line so there is room to react.
|
||||||
|
|
||||||
|
### The structural fix — §7 is the single source of truth for operational facts
|
||||||
|
|
||||||
|
**Old → new.** Specs in `docs/` carried their own copies of resource IDs,
|
||||||
|
regions, DNS records, and service state. They now **cite** §7 instead.
|
||||||
|
|
||||||
|
The reasoning is entry (q)'s DKIM inversion, generalised: the same operational
|
||||||
|
fact lived in §7 and in `docs/05`, a correction reached one of them, and the
|
||||||
|
stale copy ended up instructing an operator to delete the three records that
|
||||||
|
authenticate outbound mail — under the heading "Never delete". Same class as the
|
||||||
|
D3 amendment surviving three sweeps. **A duplicated fact is a fact that will
|
||||||
|
eventually be wrong in one place, and the copy that goes stale is the one nobody
|
||||||
|
re-reads.**
|
||||||
|
|
||||||
|
The rule is now in `CLAUDE.md` under Conventions, with that incident as its
|
||||||
|
stated reason so it is not softened later by someone who does not know the cost.
|
||||||
|
|
||||||
|
**Swept.** `docs/05` no longer restates the DKIM token sets, the DNS records, the
|
||||||
|
MX, the region, or the table name — it cites §7 and keeps only the two facts it
|
||||||
|
*depends* on, stated as dependencies. `docs/06` no longer restates the region,
|
||||||
|
bucket, distribution ID, intake endpoint, or account ID; its variable table now
|
||||||
|
points at §7 rows, and the IAM substitutions say explicitly that they are not
|
||||||
|
repeated and why. Verified by command:
|
||||||
|
|
||||||
|
```
|
||||||
|
grep -rn "ca-central-1\|adr-smlcompany-site\|E1OK7G98KNKUTA\|4tl0m5igkj\|327082975128\|adr-intake-submissions\|d26v23dhgsp2ta\|jkddzztszm" docs/*.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Output read: **no matches.** Every operational identifier in `docs/` is now a
|
||||||
|
reference.
|
||||||
|
|
||||||
|
### Q29 — the guard is widened on both counts
|
||||||
|
|
||||||
|
Both gaps `adversarial-reviewer` found are closed:
|
||||||
|
|
||||||
|
- **`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are guarded.** By Q22 nobody
|
||||||
|
has confirmed the IAM user or its key exists, so an unset key is the single
|
||||||
|
likeliest first-run failure — excluding it defeated the guard's stated purpose.
|
||||||
|
**Only emptiness is tested and no value is ever echoed**, so nothing can leak
|
||||||
|
into a run log.
|
||||||
|
- **`INTAKE_ENDPOINT` promoted to job-level `env:` and guarded.** An empty one
|
||||||
|
does not fail a build; it **ships a live contact form posting to nothing**. A
|
||||||
|
silent production defect is worse than a failed build.
|
||||||
|
|
||||||
|
The build step still reads `${{ vars.INTAKE_ENDPOINT }}` rather than the `env`
|
||||||
|
context — depending on Gitea's expression-context support is the exact thing the
|
||||||
|
guard exists not to do. Coverage checked against every `vars.*` and `secrets.*`
|
||||||
|
the workflow consumes: all guarded except `BOOKING_URL`, which is empty by
|
||||||
|
decision (R6).
|
||||||
|
|
||||||
|
### Also done
|
||||||
|
|
||||||
|
**`docs/06`: the account ID is out of the backup-bucket callout**, pointing at
|
||||||
|
§10 instead — the same removal entry (m) made in `README.md`. Operational
|
||||||
|
usefulness does not outweigh pairing a live account number with a statement about
|
||||||
|
what the account holds, in a file that gets pasted around. The billing check now
|
||||||
|
derives the account from `sts get-caller-identity` rather than hard-coding it.
|
||||||
|
|
||||||
|
**`astro.config.mjs`: `prefetch` removed entirely** — recorded here as a decision,
|
||||||
|
not a silent deletion. *Any* prefetch setting ships Astro's prefetch script to
|
||||||
|
every page, against CLAUDE.md's "default to zero JS", for a marginal gain on a
|
||||||
|
small static site already served from CloudFront. **Revisit only against real
|
||||||
|
Lighthouse numbers**, once pages exist to measure. Entry (q) had set it to
|
||||||
|
`hover`; that was still JS on every page.
|
||||||
|
|
||||||
|
**Gitea version stays `[assumed]`** at Pouya's direction — he is running the
|
||||||
|
check. The guard is designed not to need the answer.
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
The widened guard's `run:` block was **executed** under `sh -e` across four input
|
||||||
|
states — all set, all empty, secret empty, intake empty. Exit 0 only when
|
||||||
|
everything is present; exit 1 naming exactly the missing items otherwise; no
|
||||||
|
value printed in any case. `node --check astro.config.mjs` passes. The
|
||||||
|
duplication sweep was run as the `grep` above and its output read — no matches.
|
||||||
|
|
||||||
|
`npm run lint`, `check` and `build` still **cannot run**: `node_modules` is
|
||||||
|
absent and `npm ci` needs a lockfile. Installing dependencies is the first
|
||||||
|
concrete task of build step 1.
|
||||||
|
|
||||||
|
**Closed:** Q19 (and with it the last blocker on `/contact/`). **Still open:**
|
||||||
|
Q22 (deploy credential), Q23 (runner registration), Q27, Q28, R6 (parked).
|
||||||
|
**Q29 closed by implementation.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-08-26 (q) — Guard built; and the audit found an inverted DKIM table that would have broken production mail
|
## 2026-08-26 (q) — Guard built; and the audit found an inverted DKIM table that would have broken production mail
|
||||||
|
|
||||||
**Who:** Pouya ruled on entry (o)'s escalations, ratified R7, promoted R8, and
|
**Who:** Pouya ruled on entry (o)'s escalations, ratified R7, promoted R8, and
|
||||||
|
|||||||
@@ -162,6 +162,19 @@ page that collects legal inquiries.
|
|||||||
URL, Open Graph and Twitter card tags, and appropriate JSON-LD. See
|
URL, Open Graph and Twitter card tags, and appropriate JSON-LD. See
|
||||||
`docs/04-seo-spec.md`. A page without these is not finished.
|
`docs/04-seo-spec.md`. A page without these is not finished.
|
||||||
|
|
||||||
|
**`AGENTS.md` §7 is the single source of truth for operational facts.** Resource
|
||||||
|
IDs, regions, DNS records, credential state, service status — these live in §7
|
||||||
|
and nowhere else. Specs in `docs/` **cite** §7; they do not restate it. Write
|
||||||
|
"the region `AGENTS.md` §7 records", not the region. Same for bucket names,
|
||||||
|
distribution IDs, DKIM tokens, endpoints, and account identifiers.
|
||||||
|
|
||||||
|
A duplicated fact is a fact that will eventually be wrong in one place, and the
|
||||||
|
copy that goes stale is the one nobody re-reads. This rule exists because
|
||||||
|
`docs/05-backend-spec.md` carried its own copy of the SES DKIM table, a
|
||||||
|
correction reached §7 and never reached it, and the stale copy ended up telling
|
||||||
|
an operator to delete the three records that authenticate outbound mail —
|
||||||
|
under the heading "Never delete".
|
||||||
|
|
||||||
**A sweep is a command, not a claim.** Any statement that a change was applied
|
**A sweep is a command, not a claim.** Any statement that a change was applied
|
||||||
across files — a phrase removed everywhere, a path updated everywhere, a
|
across files — a phrase removed everywhere, a path updated everywhere, a
|
||||||
decision swept through the docs — must cite the command that proves it, and be
|
decision swept through the docs — must cite the command that proves it, and be
|
||||||
|
|||||||
+4
-6
@@ -31,10 +31,8 @@ export default defineConfig({
|
|||||||
service: { entrypoint: 'astro/assets/services/sharp' },
|
service: { entrypoint: 'astro/assets/services/sharp' },
|
||||||
},
|
},
|
||||||
|
|
||||||
// Opt-in only. `prefetchAll: true` with the viewport strategy injects Astro's
|
// No `prefetch` block at all — removed 2026-08-26, see AGENTS.md entry (r).
|
||||||
// prefetch script and an IntersectionObserver into EVERY page and speculatively
|
// Any prefetch setting ships Astro's prefetch script to every page, against
|
||||||
// fetches every link that scrolls into view — against CLAUDE.md's "default to
|
// CLAUDE.md's "default to zero JS", for a marginal gain on a small static site
|
||||||
// zero JS" and AGENTS.md §7's "most pages ship zero JS", with no Change Log
|
// already served from CloudFront. Revisit only against real Lighthouse numbers.
|
||||||
// entry justifying it. Add data-astro-prefetch to individual links that earn it.
|
|
||||||
prefetch: { prefetchAll: false, defaultStrategy: 'hover' },
|
|
||||||
});
|
});
|
||||||
|
|||||||
+46
-23
@@ -73,10 +73,11 @@ Client-side validation is a convenience. **The Lambda re-validates everything.**
|
|||||||
|
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
DynamoDB, `ca-central-1` — **Canadian data residency is a real selling point for
|
DynamoDB, in the region `AGENTS.md` §7 records. **Canadian data residency is
|
||||||
a Canadian legal practice, and the privacy policy will say so.** Confirm the
|
worth stating in the privacy policy**: parties describing a live dispute are
|
||||||
existing table's region and migrate if it is elsewhere. `AGENTS.md` §7 records
|
handing over sensitive material, and where it comes to rest is a fair question
|
||||||
`adr-intake-submissions` in `ca-central-1` `[verified 2026-08-26]`.
|
for them to ask. Confirm the existing table's region and migrate if it is
|
||||||
|
elsewhere — §7 has the table name and region.
|
||||||
|
|
||||||
| Attribute | |
|
| Attribute | |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -109,30 +110,52 @@ SES on submission:
|
|||||||
of the no-retainer language, and a link to the privacy policy. This email is
|
of the no-retainer language, and a link to the privacy policy. This email is
|
||||||
the reason the form beats a `mailto:` link.
|
the reason the form beats a `mailto:` link.
|
||||||
|
|
||||||
|
**SES production access is granted** (Q19, 2026-08-26) — mail reaches unverified
|
||||||
|
recipients, so the inquirer confirmation works. See §7 for the account state.
|
||||||
|
|
||||||
|
### Bounce and complaint monitoring
|
||||||
|
|
||||||
|
Configured 2026-08-26; the resource names, thresholds and current state are in
|
||||||
|
`AGENTS.md` §7. What matters here is why it is a real control rather than a
|
||||||
|
formality:
|
||||||
|
|
||||||
|
**At this volume a single bad address is a threshold event.** SES suspends
|
||||||
|
sending above roughly a 5% bounce rate. Under 100 messages a month, five bounces
|
||||||
|
crosses it — and an intake form is exactly where mistyped addresses arrive. The
|
||||||
|
alarms sit well below that line so there is room to react.
|
||||||
|
|
||||||
|
**Bounces and complaints are handled by SES email feedback forwarding**, which is
|
||||||
|
on by default, **not** by an SNS feedback topic. That is deliberate: at this
|
||||||
|
volume there is nothing to consume a programmatic feed, and an unused SNS topic
|
||||||
|
is one more thing to keep correct. Revisit when code needs to *act* on a
|
||||||
|
bounce — suppression lists, retry logic, marking a record undeliverable.
|
||||||
|
|
||||||
|
> **The alarms currently notify nobody.** §7 records the `ses-alerts` email
|
||||||
|
> subscription as **pending confirmation**. An unconfirmed SNS subscription
|
||||||
|
> drops every message, so until the confirmation link is clicked the alarms
|
||||||
|
> fire into nothing. This is the first thing to check if `/contact/` ships.
|
||||||
|
|
||||||
**Email authentication — in place as of 2026-08-26 (Q20).**
|
**Email authentication — in place as of 2026-08-26 (Q20).**
|
||||||
|
|
||||||
SPF and DMARC were both added by Pouya and independently verified on
|
SPF and DMARC are both live and independently verified (Q20); mail is on Google
|
||||||
2026-08-26. Mail is on Google Workspace (MX `1 smtp.google.com`) with Google
|
Workspace with Google DKIM configured, and the SES domain identity is verified
|
||||||
DKIM configured, and the SES domain identity is verified for sending in
|
for sending. **The record values, the MX, and the region are in `AGENTS.md` §7 —
|
||||||
`ca-central-1`. **An earlier version of this spec said neither record existed;
|
not restated here.** An earlier version of this spec asserted that neither SPF
|
||||||
that was true when written and is no longer.** The records now live are:
|
nor DMARC existed; that was true when written and is no longer, which is the
|
||||||
|
whole argument for citing §7 rather than copying it.
|
||||||
|
|
||||||
| Host | Type | Value |
|
**What is already in place.** `AGENTS.md` §7 is the record — resource IDs, DNS
|
||||||
|---|---|---|
|
records, DKIM token sets, and their verification state all live there and are not
|
||||||
| `@` | TXT | `v=spf1 include:_spf.google.com include:amazonses.com ~all` |
|
restated here. Read §7 before touching DNS.
|
||||||
| `_dmarc` | TXT | `v=DMARC1; p=none; rua=mailto:info@smlcompany.ca; fo=1` |
|
|
||||||
|
|
||||||
**What is already in place** (Namecheap DNS and the SES console, both inspected
|
Two points from §7 that this spec depends on, cited rather than copied:
|
||||||
2026-08-26):
|
|
||||||
|
|
||||||
| Record | Status |
|
- **Only one of the two SES DKIM token sets resolves.** §7 names both sets and
|
||||||
|---|---|
|
marks which is which. The resolving set is what DMARC alignment rests on;
|
||||||
| 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]` |
|
deleting it breaks intake mail authentication silently. The other set is
|
||||||
| 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]` |
|
NXDOMAIN and inert. **Do not act on any DKIM list that is not §7's.**
|
||||||
| `google._domainkey` TXT | Google Workspace DKIM. Never delete |
|
- **SES has no custom MAIL FROM**, so SPF is unaligned and SES satisfies DMARC
|
||||||
| Two CNAMEs → `jkddzztszm.acm-validations.aws` | ACM certificate validation. **Never delete** — breaks HTTPS at the next renewal |
|
through DKIM alone.
|
||||||
| `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
|
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
|
any future edit: a domain may publish **only one** `v=spf1` record, so both senders go in one
|
||||||
|
|||||||
+24
-22
@@ -121,28 +121,30 @@ permission), or grant `s3:AbortMultipartUpload` on `BUCKET_NAME/*`.
|
|||||||
|
|
||||||
**Repository → Settings → Actions → Variables** — not secrets. These are not
|
**Repository → Settings → Actions → Variables** — not secrets. These are not
|
||||||
sensitive, and keeping them as variables means they appear in run logs where
|
sensitive, and keeping them as variables means they appear in run logs where
|
||||||
they are useful for debugging. Values captured 2026-08-26 by
|
they are useful for debugging.
|
||||||
`scripts/aws-discover.sh`:
|
|
||||||
|
|
||||||
| Variable | Value |
|
| Variable | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `AWS_REGION` | `ca-central-1` |
|
| `AWS_REGION` | `AGENTS.md` §7 — Region |
|
||||||
| `S3_BUCKET` | `adr-smlcompany-site` |
|
| `S3_BUCKET` | §7 — S3 bucket |
|
||||||
| `CLOUDFRONT_DISTRIBUTION_ID` | `E1OK7G98KNKUTA` |
|
| `CLOUDFRONT_DISTRIBUTION_ID` | §7 — CloudFront |
|
||||||
| `INTAKE_ENDPOINT` | `https://4tl0m5igkj.execute-api.ca-central-1.amazonaws.com` |
|
| `INTAKE_ENDPOINT` | §7 — Intake API |
|
||||||
| `BOOKING_URL` | *(empty — parked, R6)* |
|
| `BOOKING_URL` | *(empty — parked, R6)* |
|
||||||
|
|
||||||
IAM policy substitutions: `BUCKET_NAME` = `adr-smlcompany-site`,
|
The same four values fill the IAM policy's `BUCKET_NAME`, `ACCOUNT_ID` and
|
||||||
`ACCOUNT_ID` = `327082975128`, `DISTRIBUTION_ID` = `E1OK7G98KNKUTA`.
|
`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
|
> **Read this before creating the key.** The AWS account is **not** a
|
||||||
> `meshkinilaw.ca`, `demesne.media`, `orynenergy.ca`, `lajirugs.ca`, and
|
> single-project account: it is shared with several unrelated sites and with a
|
||||||
> `mlp-clientdb-prod-backups` — which **by its name** holds another business's
|
> bucket whose name indicates another business's production client-database
|
||||||
> production client-database backups; the contents were never inspected, only
|
> backups. `AGENTS.md` §10 has the specifics and the account identifier; they
|
||||||
> the bucket name observed. A static deploy key for a marketing site lives in
|
> are kept there rather than repeated here. A static deploy key for a marketing
|
||||||
> the same account. The scoped policy is
|
> site lives in that same account, and the scoped policy is what keeps a
|
||||||
> what keeps a compromised Gitea runner from reaching any of that. Do not widen
|
> compromised Gitea runner from reaching any of it. Do not widen it, and never
|
||||||
> it, and never put the `user/pouya` credentials in CI.
|
> put the `user/pouya` credentials in CI.
|
||||||
|
|
||||||
### A runner must exist
|
### 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
|
> **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.
|
> 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
|
> 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
|
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
|
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
|
1. Re-run the workflow at the last good tag, or
|
||||||
2. `git revert` and push, or
|
2. `git revert` and push, or
|
||||||
3. Restore from S3 object versioning — **already Enabled** on
|
3. Restore from S3 object versioning — **already Enabled** on the site bucket
|
||||||
`adr-smlcompany-site` `[verified 2026-08-26 — AGENTS.md §7]`. It is the
|
(`AGENTS.md` §7). It is the
|
||||||
difference between a rollback and a rebuild; do not turn it off.
|
difference between a rollback and a rebuild; do not turn it off.
|
||||||
|
|
||||||
Then invalidate `/*`.
|
Then invalidate `/*`.
|
||||||
@@ -318,8 +320,8 @@ Then invalidate `/*`.
|
|||||||
- [ ] 404 returns a 404 status
|
- [ ] 404 returns a 404 status
|
||||||
- [ ] Security headers present (`securityheaders.com` A or better)
|
- [ ] 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 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
|
- [ ] **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 `adr-intake-submissions`, both emails delivered to a real inbox, TTL set
|
- [ ] 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
|
- [ ] 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
|
- [ ] Favicon set complete
|
||||||
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
|
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
|
||||||
@@ -330,7 +332,7 @@ Then invalidate `/*`.
|
|||||||
- [ ] Bucket not publicly readable; OAC in force
|
- [ ] Bucket not publicly readable; OAC in force
|
||||||
- [ ] ACM certificate valid; Namecheap validation CNAME still present
|
- [ ] ACM certificate valid; Namecheap validation CNAME still present
|
||||||
- [ ] CloudWatch alarms: Lambda errors, DLQ depth, 5xx rate
|
- [ ] 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**
|
**Post-cutover, same day**
|
||||||
- [ ] Sitemap submitted to Google Search Console and Bing Webmaster Tools
|
- [ ] Sitemap submitted to Google Search Console and Bing Webmaster Tools
|
||||||
|
|||||||
Reference in New Issue
Block a user