Repository navigation
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
Conversation
…y_email, the deployment-level sibling of sys_email (WIP) Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
…il and the 18.0 sys_email dual read Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
…ail, not sys_email Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
… held equal to sys_email's; regenerate the tenant-audit census Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
…0d-identity-mail-sibling
Claude-Session: https://claude.ai/code/session_016CvQEdxxYYVa8C8AjKZBtL Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 64 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 13 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 153 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b227d7415381da4f6c3744e94dd2d71b81588698 && git checkout b227d7415381da4f6c3744e94dd2d71b81588698
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2667e2402c48e9201550caca2adbcf9188d925b3 cd6fe0b4fbeb9c1b50160a05d6972197daf647cb && git checkout -B drift-repro 2667e2402c48e9201550caca2adbcf9188d925b3 && git merge --no-ff cd6fe0b4fbeb9c1b50160a05d6972197daf647cb
node scripts/docs-audit/affected-docs.mjs --json 2667e2402c48e9201550caca2adbcf9188d925b3
|
…0d-identity-mail-sibling
Contract reviewServed-tier: Reviewer: an isolated at-tier reviewer subagent of the adopting seat ( Check-runs on ① Derived judgments(a) Fresh install — every identity send lands in the sibling, with no organization. HOLDS.
(b) 18.0 database — every outbox reader covers both tables. HOLDS. The two logs are named once in
(c) Access — who may read the sibling. HOLDS. (d) Nothing moves or deletes 18.0 rows. HOLDS. No migration, no data move, no column change on (e) R1-R4. The addendum reports the harness 8/8 on (f) Complete feature (6270035914). HOLDS. New installs write the new shape, the 18.0 shape is still read, and nothing waits on a later PR: the object, the contract member, the five producers, the five readers, the gate, the inventory row, the census and the docs land together, exercised end to end on the real stack by the dogfood census. No dormant code: ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
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
mainand 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 assys_email; a new test holds the two equal. It declaressystemFields: { tenant: false }, so it has no organization column. It ismanagedBy: 'append-only', exposesapiMethods: ['get', 'list'], and requiresmanage_platform_settingsto read, likesys_job, because its rows carry reset and sign-in links.EmailServicePluginregisters it besidesys_email.Contract:
SendEmailInput.identityMail?: booleanandSendTemplateInput.identityMail?: boolean(packages/spec/src/contracts/email-service.ts).truesends the row tosys_identity_email.sys_email, unchanged.identityMail: truetogether withorganizationIdis refused asVALIDATION_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.tsnames the two logs once. The following now read and write the log the row lives in:send()andsendTemplate();insert/update/readHeadersJsontake the log as an optional last argument);email.send.asyncworker;afterInsertoutbox drain hook;Job payloads work like this:
object: 'sys_identity_email'.sys_emailjob keeps the exact 18.0 payload ({ rowId }). A job with noobjectreadssys_email.errorand touches no row. A reclaim job of that kind stalls; it is not re-armed.plugin-auth: five identity sends now say
identityMail: trueand name no organization, in every posture:auth.verify_email,auth.password_reset,auth.account_invitation,auth.magic_linkandauth.email_change_notice. The now-unusedauthMailOrganizationArg/derivableWriteOrganizationIdare removed.auth.invitation(organization invitation) is unchanged tenant mail.Inventory:
sys_identity_emailgets its row next tosys_email's inorganization-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).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.mdxnames the sibling.18.0-row behaviour (expand-first)
Nothing is moved, deleted or re-stamped. These keep working from
sys_email:single;{ rowId }jobs an 18.0 process queued for them.The boot sweep still delivers them, the worker still delivers a job with no
objectfromsys_email, and reclamation of a job with noobjectstill readssys_email. Pinned against an 18.0-shaped fixture (rows seeded intosys_emailwithrelated_object: 'sys_user'and no organization, plus a raw{ rowId }job) inemail-plugin.outbox-sweep.test.ts. The #24522 harness is not merged yet; the seat will ask for amainmerge and a harness run when it is.What the step-5 ceremony owes (debt recorded, nothing done here)
sys_email: move the identity rows tosys_identity_email, withoutorganization_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. Undersinglean 18.0 install stamped them with the Default Organization, so the ceremony must decide whether those move too. The ruling names "walled rows anchored onsys_useror on nothing".sent/failed).{ rowId }job readssys_email. After a move it would find no row, and the message would never be delivered.objectreadssys_email. After a move it would take the row-gone branch and delete that row's attachment content. Either rewrite those jobs' payloads to addobject, or move rows only after their reclaim window has passed.sys_activity: rows whoseobject_name/source_objectissys_emailfor 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_idNOT NULL (8e): only once the move leaves zero NULL rows.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.docs/adr/**is outside this card.Verification (head
3bc88f5f1, after mergingmainatba4afcc23)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. Thesrc/auditsubset was re-run on the final head: 27 passed.@objectstack/spec:platform-object-names.test.tsandemail-service.test.tspassed (15);typecheckpassed.@objectstack/cliunit:organization-ownership-inventory.test.ts, 25 passed.signup-organization-census.dogfood.test.ts, 3 passed. It now asserts:sys_identity_emailand absent fromsys_email;organization_id, withsys_email's having it as the non-vacuity control.pnpm dev -- --fresh -p 38531, sign-in as the seeded admin):POST /auth/request-password-resetproduced onesys_identity_emailrow,Reset your ObjectStack password,sent,related_object: sys_user, noorganization_idkey.sys_identity_emailrow,Verify your ObjectStack email address,sent, noorganization_idkey.POST /auth/organization/invite-memberproduced onesys_emailrow,Dev Admin invited you to Default Organization,related_object: sys_invitation, stampedorganization_id.sys_emailheld no identity mail. The server was stopped by its recorded PID and the port is free.scripts/ablation-replace.mjs, then the tests ran, then the file was restored to the HEAD blob withgit diff HEADempty.sys_email: 4 red.object: 2 red.objectreads the sibling: 11 red, including the 18.0{ rowId }pin.sys_email: 2 red.identityMailfrom 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.dist/: the dogfood census went 2 red, then 3 green after the restore and rebuild, withablation-dist-preflightpresent / absent. The first A6 attempt's mutated build failed on a TS no-overlap error, so that reading is void.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 126 derived. All 126 were run on3bc88f5f1with exit codes captured before any pipe: 126 exit 0.--ranreconciliation: 126 derived, 126 run, 0 NOT MEASURED. Pluscheck:dev-prereqsexit 0. The 51 artifact-roster families, the CI-only jobs and the type-check lanes are CI's.Acceptance notes
sys_identity_emailgets no translation bundle entries, likesys_platform_setting; its labels are English in every locale.check:keyed-text-boundsreads only a literalfields, so the sibling spells its fields out instead offields: SysEmail.fields. The equality test is what keeps the row shape single-sourced.sys_email/attachments/prefix for sibling rows. Keys travel in the row and the job, so nothing reads the prefix as a table name.sys_emailrows (it has norequiredPermissions). 18.0 identity rows there carry reset links. The sibling is gated.Generated by Claude Code