Repository navigation
Commit fbafb85
feat(plugin-email,plugin-auth): identity mail persists to sys_identity_email, the deployment-level sibling of sys_email (18.x step 3, 8b-0d′) (#24587)
Closes #24531
Clause-②: yes (widening)
18.x step 3 (8b-0d′), per ruling of record 6257510973 on #24377 (letter
Q2 A) and ruling records 6266750393 and 6267391172 on #15194. **Merge
gate:** this PR stays a draft until #24522's upgrade harness has merged.
It then merges `main` and lands only when R1–R4 hold with the harness
green.
## What changed
- **New object `sys_identity_email`**
(`packages/platform-objects/src/audit/sys-identity-email.object.ts`). It
has the same fields and indexes as `sys_email`; a new test holds the two
equal. It declares `systemFields: { tenant: false }`, so it has **no
organization column**. It is `managedBy: 'append-only'`, exposes
`apiMethods: ['get', 'list']`, and requires `manage_platform_settings`
to read, like `sys_job`, because its rows carry reset and sign-in links.
`EmailServicePlugin` registers it beside `sys_email`.
- **Contract:** `SendEmailInput.identityMail?: boolean` and
`SendTemplateInput.identityMail?: boolean`
(`packages/spec/src/contracts/email-service.ts`).
- `true` sends the row to `sys_identity_email`.
- Absent sends it to `sys_email`, unchanged.
- `identityMail: true` together with `organizationId` is refused as
`VALIDATION_FAILED`. The refusal is recorded as a rejected row in the
sibling, with no organization written.
- **plugin-email: one pipeline over both logs.** `email-log-objects.ts`
names the two logs once. The following now read and write the log the
row lives in:
- `send()` and `sendTemplate()`;
- the persistence shim (`insert` / `update` / `readHeadersJson` take the
log as an optional last argument);
- the `email.send.async` worker;
- the `afterInsert` outbox drain hook;
- the boot outbox sweep, which sweeps each log on its own;
- attachment reclamation and the internal-header readback.
Job payloads work like this:
- A delivery or reclaim job for a sibling row carries `object:
'sys_identity_email'`.
- A `sys_email` job keeps the exact 18.0 payload (`{ rowId }`). A job
with no `object` reads `sys_email`.
- A job naming anything else is refused at `error` and touches no row. A
reclaim job of that kind stalls; it is not re-armed.
- **plugin-auth:** five identity sends now say `identityMail: true` and
name no organization, in every posture: `auth.verify_email`,
`auth.password_reset`, `auth.account_invitation`, `auth.magic_link` and
`auth.email_change_notice`. The now-unused `authMailOrganizationArg` /
`derivableWriteOrganizationId` are removed. `auth.invitation`
(organization invitation) is unchanged tenant mail.
- **Inventory:** `sys_identity_email` gets its row next to `sys_email`'s
in `organization-ownership-inventory.ts` (`column-drop`, `PMAIN`).
- **Regenerated with the repo's tooling:**
- `scripts/platform-object-tenancy-census.json`
(`gen:platform-object-tenancy-census`: 84 registered, 40 out of reach).
- The tenant-audit census (`tenant-audit-census.mjs --write`). Three
write sites in plugin-email now name their object at run time: the
persistence insert, the persistence update and the reclaim update, so
160 decidable became 157 and 71 undecidable became 74. Two hand-written
prose numbers on that page are updated to match.
- **Docs:** `content/docs/plugins/packages.mdx` names the sibling.
## 18.0-row behaviour (expand-first)
Nothing is moved, deleted or re-stamped. These keep working from
`sys_email`:
- organization-less identity rows an 18.0 install wrote there under a
wall;
- identity rows stamped with the Default Organization under `single`;
- the `{ rowId }` jobs an 18.0 process queued for them.
The boot sweep still delivers them, the worker still delivers a job with
no `object` from `sys_email`, and reclamation of a job with no `object`
still reads `sys_email`. Pinned against an 18.0-shaped fixture (rows
seeded into `sys_email` with `related_object: 'sys_user'` and no
organization, plus a raw `{ rowId }` job) in
`email-plugin.outbox-sweep.test.ts`. The #24522 harness is not merged
yet; the seat will ask for a `main` merge and a harness run when it is.
## What the step-5 ceremony owes (debt recorded, nothing done here)
- **`sys_email`: move the identity rows** to `sys_identity_email`,
without `organization_id`. These are rows about a person:
`related_object = 'sys_user'`, or no related object (the magic link).
Under a wall they are organization-less. Under `single` an 18.0 install
stamped them with the Default Organization, so the ceremony must decide
whether those move too. The ruling names "walled rows anchored on
`sys_user` or on nothing".
- Move only terminal rows (`sent` / `failed`).
- A queued row's pending `{ rowId }` job reads `sys_email`. After a move
it would find no row, and the message would never be delivered.
- A pending reclaim job with no `object` reads `sys_email`. After a move
it would take the row-gone branch and **delete** that row's attachment
content. Either rewrite those jobs' payloads to add `object`, or move
rows only after their reclaim window has passed.
- **`sys_activity`:** rows whose `object_name` / `source_object` is
`sys_email` for a moved row point at a missing record. Re-point them or
delete them; Q3 A's null-activity deletion already covers the
organization-less ones.
- **`sys_email.organization_id` NOT NULL (8e):** only once the move
leaves zero NULL rows.
- **Step 7 (dual reads):** the "no `object` ⇒ `sys_email`" default stays
correct for tenant jobs. Once no 18.0 job can still be pending, nothing
identity-specific is left to remove.
- **Outside this repo:** cloud's wall-test control is rewritten against
the sibling (cloud lane, per the ruling). ADR-0131 D7's Q2 A note says
"has not shipped" and needs its dated update; that is Tier H, and
`docs/adr/**` is outside this card.
## Verification (head `3bc88f5f1`, after merging `main` at `ba4afcc23`)
- `pnpm --filter @objectstack/plugin-email typecheck` + `vitest run`: 34
files, 571 passed.
- `pnpm --filter @objectstack/plugin-auth typecheck` + `vitest run`: 165
files, 3168 passed, 10 skipped. That run was before the merge; `main`'s
two commits do not touch plugin-auth. The targeted auth mail suites were
re-run on the final head: 77 passed.
- `pnpm --filter @objectstack/platform-objects typecheck` + `vitest
run`: 78 files, 1188 passed. The `src/audit` subset was re-run on the
final head: 27 passed.
- `@objectstack/spec`: `platform-object-names.test.ts` and
`email-service.test.ts` passed (15); `typecheck` passed.
- `@objectstack/cli` unit: `organization-ownership-inventory.test.ts`,
25 passed.
- Dogfood (real stack): `signup-organization-census.dogfood.test.ts`, 3
passed. It now asserts:
- the sign-up's verification mail is in `sys_identity_email` and absent
from `sys_email`;
- the sibling's registered schema has no `organization_id`, with
`sys_email`'s having it as the non-vacuity control.
- **Fresh showcase boot** (`pnpm dev -- --fresh -p 38531`, sign-in as
the seeded admin):
- `POST /auth/request-password-reset` produced one `sys_identity_email`
row, `Reset your ObjectStack password`, `sent`, `related_object:
sys_user`, no `organization_id` key.
- A sign-up after opening the audience produced one `sys_identity_email`
row, `Verify your ObjectStack email address`, `sent`, no
`organization_id` key.
- `POST /auth/organization/invite-member` produced one `sys_email` row,
`Dev Admin invited you to Default Organization`, `related_object:
sys_invitation`, stamped `organization_id`.
- `sys_email` held no identity mail. The server was stopped by its
recorded PID and the port is free.
- **Ablations.** Each mutation went through
`scripts/ablation-replace.mjs`, then the tests ran, then the file was
restored to the HEAD blob with `git diff HEAD` empty.
- A1, route every send to `sys_email`: 4 red.
- A2, the worker ignores the job's `object`: 2 red.
- A3, a job with no `object` reads the sibling: 11 red, including the
18.0 `{ rowId }` pin.
- A4, reclaim always reads `sys_email`: 2 red.
- A5, drop `identityMail` from the verification send (plugin-auth): 3
red. The first A5 attempt was refused by the tool (the replacement was a
substring of the anchor) and measured nothing.
- A6, the same mutation in plugin-auth's `dist/`: the dogfood census
went 2 red, then 3 green after the restore and rebuild, with
`ablation-dist-preflight` present / absent. The first A6 attempt's
mutated build failed on a TS no-overlap error, so that reading is void.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, 126 derived. All 126 were run on
`3bc88f5f1` with exit codes captured before any pipe: 126 exit 0.
`--ran` reconciliation: 126 derived, 126 run, 0 NOT MEASURED. Plus
`check:dev-prereqs` exit 0. The 51 artifact-roster families, the CI-only
jobs and the type-check lanes are CI's.
## Acceptance notes
- `sys_identity_email` gets no translation bundle entries, like
`sys_platform_setting`; its labels are English in every locale.
- `check:keyed-text-bounds` reads only a literal `fields`, so the
sibling spells its fields out instead of `fields: SysEmail.fields`. The
equality test is what keeps the row shape single-sourced.
- Out-of-row attachment storage keys keep the `sys_email/attachments/`
prefix for sibling rows. Keys travel in the row and the job, so nothing
reads the prefix as a table name.
- Not measured: whether a non-administrator can list `sys_email` rows
(it has no `requiredPermissions`). 18.0 identity rows there carry reset
links. The sibling is gated.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5e80009 commit fbafb85
27 files changed
Lines changed: 1111 additions & 291 deletions
File tree
- .changeset
- content/docs
- permissions
- plugins
- docs/audits
- packages
- cli/src/utils
- platform-objects/src/audit
- plugins
- plugin-auth/src
- plugin-email/src
- qa/dogfood/test
- spec/src
- contracts
- system/constants
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
189 | 189 | | |
190 | 190 | | |
191 | 191 | | |
192 | | - | |
| 192 | + | |
193 | 193 | | |
194 | 194 | | |
195 | 195 | | |
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
204 | | - | |
| 204 | + | |
205 | 205 | | |
206 | 206 | | |
207 | 207 | | |
| |||
224 | 224 | | |
225 | 225 | | |
226 | 226 | | |
227 | | - | |
228 | | - | |
229 | | - | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
230 | 230 | | |
231 | 231 | | |
232 | 232 | | |
| |||
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | | - | |
248 | | - | |
| 247 | + | |
| 248 | + | |
249 | 249 | | |
250 | | - | |
| 250 | + | |
251 | 251 | | |
252 | 252 | | |
253 | 253 | | |
| |||
297 | 297 | | |
298 | 298 | | |
299 | 299 | | |
300 | | - | |
| 300 | + | |
301 | 301 | | |
302 | 302 | | |
303 | 303 | | |
304 | | - | |
| 304 | + | |
305 | 305 | | |
306 | | - | |
| 306 | + | |
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
438 | 438 | | |
439 | 439 | | |
440 | 440 | | |
441 | | - | |
| 441 | + | |
442 | 442 | | |
443 | 443 | | |
444 | 444 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
39 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
97 | | - | |
| 97 | + | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
160 | | - | |
161 | | - | |
162 | | - | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
163 | 163 | | |
164 | 164 | | |
165 | 165 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
544 | 544 | | |
545 | 545 | | |
546 | 546 | | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
547 | 553 | | |
548 | 554 | | |
549 | 555 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
0 commit comments