Uh oh!
There was an error while loading. Please reload this page.
feat(spec,plugin-email,service-messaging,plugin-auth): widen SendEmailInput with optional organizationId, threaded from org-holding producers - #11839
Conversation
…lInput with optional organizationId and thread it from org-holding producers Fixes#11741 (Decision 2 of #11303). SendEmailInput/SendTemplateInput gain an optional organizationId; plugin-email's writer stamps it verbatim onto sys_email.organization_id (pass-through only — no in-adapter resolution or fabrication); the messaging email channel threads delivery.notification.organizationId on both arms; plugin-auth's invitation mail threads the invitation's own organizationId. Org-less callers (auth verification / password-reset mail) stay legal and unstamped. Forward-stamping only — no backfill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
…eeps TEST_DEBT at its frozen 97) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
📓 Docs Drift CheckThis PR changes 4 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 131 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 5bb0d52729a823a56b1219d4bbd3e893010e4554 && git checkout 5bb0d52729a823a56b1219d4bbd3e893010e4554
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9bc403ef745beea5b60ddc0048ca980214e660d8 5df5509610e3e678860c9dcfee8b7082b24f602e && git checkout -B drift-repro 9bc403ef745beea5b60ddc0048ca980214e660d8 && git merge --no-ff 5df5509610e3e678860c9dcfee8b7082b24f602e
node scripts/docs-audit/affected-docs.mjs --json 9bc403ef745beea5b60ddc0048ca980214e660d8
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11741
Clause-②: yes — public contract widening; the PR stays draft, the contract-review chain runs before enqueue.
The ruling this carries out (Decision 2 of #11303)
Maintainer, 2026-08-24, live PM chat: 「其他按照你的建议继续」, recorded on #11303 at
5396411781, Decision 2 verbatim:Parent ruling (#11303,
5393621706): 「11303 sys_inbox_message/sys_notification/sys_email 应该写 organization_id。」 Backfill posture: forward-stamping only, no backfill of existing org-less rows (maintainer 2026-08-23 precedent 「10950 不考虑存量」). Nothing here touches merged PR #11698.What changed
packages/spec/src/contracts/email-service.ts—SendEmailInput.organizationId?: stringandSendTemplateInput.organizationId?: string(optional; pass-through only; TSDoc states the no-fabrication rule and the absent-stays-legal rule; the SendTemplateInput doc distinguishes it from the pre-existingorgoverlay key).packages/plugins/plugin-email/src/email-service.ts—send()stampsinput.organizationIdverbatim onto the persistedsys_emailrow (organization_id); absent writes nothing.sendTemplate()forwards the key into theSendEmailInputit builds. No in-adapter resolution: the writer runs under a constant SYSTEM context and only passes through what the input carries. The durable paths need no further change — queue/boot-sweep delivery re-reads the row, and the terminal status update never rewritesorganization_id.packages/services/service-messaging/src/email-channel.ts— the named producer. Both arms threaddelivery.notification.organizationId: the plainsendarm and thesendTemplatetemplate arm; theEmailSenderSurfacestructural mirrors declare the key.packages/plugins/plugin-auth/src/auth-manager.ts—sendInvitationEmailthreadsinvitation.organizationId(the one auth producer that holds a real organization). All org-less auth mail is untouched.@objectstack/specminor + the three touched packages (launch-window convention: no major; additive, no ADR-0087 disposition owed —node scripts/check-adr-0087-registration.mjsexit 0).Caller census (the card marked it unmeasured)
Producers that HOLD an organization — now stamp:
packages/services/service-messaging/src/email-channel.ts:229(send call); org onchannel.ts:32delivery.notification.organizationIdpackages/services/service-messaging/src/email-channel.ts:188(sendTemplate call)delivery.notification.organizationIdsendInvitationEmailpackages/plugins/plugin-auth/src/auth-manager.ts:2672invitation.organizationIdsendTemplate→send()(internal producer)packages/plugins/plugin-email/src/email-service.ts:1281input.organizationIdProducers genuinely WITHOUT one — unchanged, absent stays legal (the ruling's named class):
sendResetPassword(auth-manager.ts:1254),sendVerificationEmail(:1311),sendMagicLink(:2833),sendChangeEmailNotice(:3391) — user-scoped auth mail.email-plugin.ts:544) — operator test message.email-plugin.ts:755) — rawSendEmailInputpass-through; carries whatever its producer wrote, no change needed.dispatchDue(report-service.ts:742,757) — holds onlyreport.owner_id(a user id); the owner-context resolver is wiredundefined(reports-plugin.ts:137, scheduled runs fail closed), so no organization value is in hand at the send site, and deriving one would be the resolution the ruling forbids.POST /email/send(rest-server.ts:8334) — spreads the caller's body verbatim, so a body carryingorganizationIdnow passes through with zero code change; whether the route should additionally stamp the execution context's tenant when the body omits one (the existingsentBysymmetry) is recorded as an open question in the dev report, not guessed at here.Out-of-scope finding filed while sweeping: #11832 —
SendTemplateInput.org("org-overlay resolution (when supported)") has zero readers in the only IEmailService implementation.Pins
organization_id; sendTemplate forwarding; plugin-auth invitation).organization_idand are NOT refused (plugin-email, messaging both arms, plugin-auth reset mail).packages/spec/src/contracts/email-service.test.ts— the pre-widening shape stays legal byte-identically; the widened shape carries the optional key on both inputs. The pre-existing exact-shapetoEqualpins inemail-channel.test.tsdouble as byte-identity evidence for org-less callers.Reverse verification (pins written first, run against unfixed source)
RED (before the fix, value dropped/never carried —
expected undefined to be 'org_apex'):email-channel.test.ts: 2 failed (both arms' identity pins) | 15 passedemail-service.test.ts+send-template.test.ts: 2 failed (row stamp; sendTemplate forwarding) | 43 passedauth-manager.test.ts: 1 failed (invitation threading) | 246 passedGREEN (after the fix, same commands): messaging 17/17 · plugin-email 45/45 · plugin-auth 247/247.
Verification (all readings at head
5df550961unless noted)pnpm --filter @objectstack/spec test— 421 files / 11221 passed;pnpm --filter @objectstack/spec typecheck— OK (incl. check:test-typecheck).pnpm --filter @objectstack/plugin-email test— 27 files / 429 passed; typecheck OK.pnpm --filter @objectstack/service-messaging test— 27 files / 279 passed; typecheck OK.pnpm --filter @objectstack/plugin-auth test— 1519 passed (suite) and 247/247 on the pinned file at head; typecheck OK (after building the package's own dist — the examples tsconfig resolves the package by name).pnpm exec turbo run typecheck --concurrency=2— 129/129 tasks successful (covers every downstream consumer of the widened spec surface).pnpm --filter @objectstack/spec check:generated— "All 14 generated artifacts are up to date."node scripts/pm/dispatch-gates.mjsderivation line, quoted: "dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 5df5509 (/home/user/objectstack-11741)." Every derived family ran locally at that head, all exit 0: changeset-gate-self-tests, cross-package-test-inputs (both spellings), doc-formula-expressions, spec empty-state / liveness / strictness-ledger / variant-docs, merge-driver, objectui-changeset, published-files, slot-lookup, spec-parsed-alias, test-source-alias, type-source-resolution, adr-0087-registration, changeset-no-major, empty-changeset, dev-prereqs, plugin-teardown-shape, docs-audit affected-docs + drift-comment, release-rehearsal-clone --self-test; convention families: query-options-erasure, type-check-coverage, type-check-debt (--re-measure OK, none above recorded — the first run caught +3 in plugin-auth's frozen TEST_DEBT from this PR's own new tests; fixed by type-clean mock access, re-measured back to the recorded 97), engine-double-contract, where-matcher, check:i18n, check:nul-bytes.check-dev-prereqsred was the unbuilt fresh worktree (40/67 packages without dist), green after the full packages build — worktree state, not diff state.Generated by Claude Code