docs(website): §7 rewritten for SMTP, plus an upgrade-notes home

Engagement Phase 1's documentation (ENGAGEMENT.md §6.0b, row 1).

BACKEND_DESIGN.md §7 is REWRITTEN rather than amended, as the plan
requires: it documented Gmail OAuth2 as the mechanism. It now covers the
transport registry and why credentialFields is a single declaration
three consumers read, the credential blob and its fail-safe decrypt, the
no-phone-home rule and its CI guardrail, the three SMTP postures, why
Send test is the only verification left, the five failure contracts, and
the silent upgrade. The §2 tree, the PR-4 route-count note, the contact
row and the dashboard row are corrected alongside it — the dashboard now
returns warnings[].

UPGRADE_NOTES.md is new, and is the home for every later phase's
operator note as well (1b, 6, 10 and 13 each owe one). Newest first, and
scoped deliberately: an upgrade that needs nothing does not get an entry.
The entries worth writing are the ones that fail QUIETLY, which is
exactly what the Gmail removal does — mail stops, nothing errors, and
the only signal is a dashboard warning.

ENGAGEMENT.md records Phase 1 as built, with the four things that are
not what the plan says: only the transport half of §3.1 was built (a
channel nothing calls is a shape frozen too early), `enabled` now gates
every sender where it used to gate none of the direct ones, the G22
warning reads the deprecated column on purpose, and `npm run swagger`
had to be fixed first — the "per-file route limit" phases 8 and 9
recorded does not exist, and the real rule matters to every later phase
here, all of which add routes.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-28 21:01:19 -05:00
parent 7a2ad727cb
commit 6f6b54a509
4 changed files with 180 additions and 15 deletions

View File

@@ -1,6 +1,7 @@
# The Engagement System — findings and plan
**Status:** design of record for the next workstream. No code written yet. The scope decisions below are
**Status:** design of record. **Phase 1 is built** (website#165 + docs#178, with website#164 as its
prerequisite); everything from Phase 1b on is still design. The scope decisions below are
settled; **four of the eight questions in §7.1 were answered by the org lead on 2026-08-28** — Q1, Q3, Q5
and Q7, and Q1's answer added a whole phase (**Phase 1b**, unique email addresses). Q2, Q4, Q6 and
Q8 remain open and block Phases 4, 5b, 2 and 8 respectively. Per CLAUDE.md § Conventions, no implementation
@@ -1236,7 +1237,7 @@ Phase 4**, **Q4 before Phase 5b**, **Q8 before Phase 8**.
---
### Phase 1 — Remove Gmail OAuth2; `DeliveryChannel` + SMTP
### Phase 1 — Remove Gmail OAuth2; the transport registry + SMTP
**This phase is a subtraction and a replacement in one PR**, because leaving the OAuth2 flow half-wired
across a release is worse than either end state.
@@ -1261,6 +1262,41 @@ deployment does today — contact form falls back to `mailto`, invites surface t
**Guardrails:** swagger regen + `routes:manifest --check` (two routes removed); no-hardcoded-host check
(§3.2 rule 4) — which the deleted `smtp.gmail.com` literal is the first real test of.
#### As built (website#165, docs#178)
Delivered as specified, with four things worth recording because they are not what the plan above says.
1. **Only half of §3.1 was built, deliberately.** `registerMailTransport` ships;
`registerDeliveryChannel` does not. Phase 1 has no consumer for `addressFor`/`render`/`deliver`
the engine that calls them is Phase 4 — and a registered channel nothing calls is a shape frozen
before anything tried to use it. It arrives with the phase that consumes it. `credentialFields` is
the piece that mattered here, since it is what makes the admin form generic.
2. **`enabled` now gates every sender, which it did not before.** `buildTransport()` used to test only
"is there a refresh token and a sender", so the contact form kept sending after an admin unticked
*Enable email sending*`isConfigured()` honoured the toggle but the five direct senders bypassed
it. The connect flow used to set `enabled` as a side effect of a consent redirect; with a credential
form it has to mean what it says, so the gate moved onto the one path every sender shares. **A
deliberate behaviour change, not a refactor**, and the only one in the phase.
3. **The G22 warning reads the deprecated column.** `refresh_token_enc` stays unread *as
configuration*, but its presence is exactly "this deployment had working mail before the upgrade",
which is the warning's whole condition. `hadLegacyConnection && !hasCredential` fires for the one
deployment this happens to and stays silent on a fresh install, which has never had mail and would
only learn to ignore the banner. The warning clears itself once a credential is saved — nothing has
to remember to dismiss it.
4. **`npm run swagger` was already broken and had to be fixed first** (website#164, its own PR). It
died with swagger-autogen's "invalid array length" runaway on a pristine `edge`, under Node 20 and
24, and at the commit whose own PR last regenerated the spec — so no phase that touches a route
could have met the standing obligation. Bisected to one statement in `teams.router.js`:
`param('teamId').custom((v) => v === 'default' || TEAM_ID.test(v))`. **The rule is that nothing
reaching `.test(` may sit inside a route statement**, and the "per-file route limit" that phases 8
and 9 recorded does not exist — a three-route file carrying only that one route dies too. Worth
knowing for every later phase in this workstream, all of which add routes.
Two smaller decisions: the credential is one encrypted JSON blob rather than a column per field, so
adding a transport is never a schema change; and a blob that will not decrypt reads as **absent**
rather than raising, so a rotated `SECRET_ENC_KEY` lands an admin on an "unconfigured" screen instead
of a 500 that takes the contact form with it.
---
### Phase 1b — Unique, changeable, verifiable email addresses *(decision 6)*