Repository navigation
feat(plugin-email,plugin-auth)!: close the sys_email_template organization door; retire the provenance stamp and the auth SMS seed (ADR-0131 C4, stage 1) - #22087
Conversation
…retire the provenance stamp An organization's create and update of a sys_email_template row are refused with PERMISSION_DENIED / 403 naming the closed door (ADR-0131 D6, ruling C on section 6 Q1). System-context writes pass: the seeds, the boot sweep, the live projector of a Studio save. The provenance stamp, which marked such an edit customized, retires with its exports. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…render per locale rung A fresh boot with phone sign-in on writes no sys_notification_template row. Each locale rung renders an operator's active row, or the built-in text where no row exists at that locale: byte for byte what the seeded store rendered. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…rganization door Cases 1 and 2 (a Studio save reaches the mail and survives a cold boot) are unchanged. Case 3 pinned the retired behaviour, a data-door edit stamped customized; it now pins the refusal of an edit and a create, and that the metadata-door wording survives the next cold boot. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…door on the docs pages The dated note names ruling C (decision card 22005, record 6020178017) and the two symbols the C4 retirement list gains; the ADR text is otherwise unchanged. The isSystem census row 58 anchors the closed sys_email_template door, the email-templates page says where a template is edited now, and the 14744 audit probe drops its retired plugin-email subject. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…rganizations may edit a row The organization door on sys_email_template is closed (ADR-0131 D6, ruling C), so the is_system help's 'tenants may edit' and the customized help's 'set when an admin edits' became false. Both say what holds now, in the four bundled locales; the en bundle is regenerated by check-i18n-bundles --write. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…form-objects patch Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…gine The ablation that put the seed back stayed green: AuthManager reads its engine from the 'data' service, which the harness did not answer, so the restored seed had no engine to write through. The harness now answers both. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…write site retired node scripts/tenant-audit-census.mjs --write regenerated the census tables (232 write call sites, one fewer: the retired seedPhoneSmsTemplates insert), and the page's hand-written figures follow the census. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
…where-matcher contracts The boot harness routes findOne and update through ObjectQL's own dispatch predicates (ledger recorded by check-engine-double-contract --write), and both row stores refuse a where combinator instead of reading it as a field name. Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 42 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 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 9ac213cfa741e0ab1dd1b52c732dece00477ab3d && git checkout 9ac213cfa741e0ab1dd1b52c732dece00477ab3d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 f439a1e3e8b159750a1f1896fe9d7ffcf9a512f1 && git checkout -B drift-repro 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770 && git merge --no-ff f439a1e3e8b159750a1f1896fe9d7ffcf9a512f1
node scripts/docs-audit/affected-docs.mjs --json 7ef50a4fbbf9c819a550b2943d2ac74a7bd39770
|
维护者速读(终稿)· PR #22087(C4 第一段,#15205)
Generated by Claude Code |
Part of #15205
Clause-②: no (narrowing)
Stage 1 of ADR-0131 card C4 (#15205), under the maintainer's ruling C on ADR-0131 §6 Q1 (decision card #22005): email templates are not overridden per organization. What stops being accepted: an organization's create and update of a
sys_email_templaterow are refused with403 PERMISSION_DENIED, and the message names the closed door. System-context writes still pass: the built-in seed, the declared-template boot sweep, the live projection of a Studioemail_templatesave into its sending row, and later the v18 migration ceremony's promotion (#15211). The template provenance stamp retires, and with it three published exports of@objectstack/plugin-email:bindEmailTemplateProvenanceStamp,unbindEmailTemplateProvenanceStampandEMAIL_TEMPLATE_PROVENANCE_PACKAGE. A boot with phone sign-in on no longer seeds the built-in auth SMS texts intosys_notification_template. What renders unchanged: every email template (the loader still readssys_email_template, and a Studio edit still reaches the mail at once and survives a restart); every built-in auth SMS text, byte for byte, at every recipient locale; and anysys_notification_templaterow an operator already has, which still wins.This PR carries a Tier H ADR edit (a dated note under ADR-0131 §6 Q1), so it needs the maintainer's approval to merge.
The loader half of the card (stage 2: the registry loader,
seedTemplates, the boot sweepbootstrapEffectiveEmailTemplates/EffectiveEmailTemplateSources, the live projector) waits on #15206 (C5). Undersingle, a Studio save of anemail_templatelands organization-scoped today, and no registry read that omits the organization sees it, live or after a cold boot (measured ate67ba80049; the stop report is on #15205). References: #15194 (the ADR-0131 execution tree), #21785 and #22062 (the current shape of the boot sweep, unchanged here).What changes
plugin-email: the door. New modulesrc/email-template-door.ts(not exported) registersbeforeInsert/beforeUpdatehooks onsys_email_template, at priority 10. A write is refused when its hook session names a caller and is not system-elevated. A write with no execution context at all (no caller) passes, the same scope ADR-0123 D2 draws. Delete is not part of this door.EmailServicePluginbinds the door where it bound the stamp, and unbinds it indestroy().plugin-email: the stamp retires.src/email-template-provenance.ts, its per-row test and its three exports are deleted. With the door closed, no non-system update reaches the engine's write, so nothing marks a rowcustomizedany more. Rows already marked keep their mark, and the boot seeders still skip them.plugin-auth: the SMS seed retires.seedPhoneSmsTemplatesand itskernel:readycall are gone.resolvePhoneSmsTemplateBodywalks the locale chain one rung at a time. At each rung it uses an active, non-blank row at that locale; otherwise the built-in text at that locale, when no row exists there at all; otherwise it moves to the next rung. The built-in walk is the floor, as it is on a failed read. That is exactly what the seeded store rendered: the seed inserted the built-in text wherever no row existed, and a deactivated or blank row passed its rung on. Nothing in plugin-auth's package entry changes.bootstrapEffectiveEmailTemplates/EffectiveEmailTemplateSources. The ADR text is otherwise unchanged.content/docs/permissions/system-context.mdxrow 58 now anchors the door (email-template-door.ts#isOrganizationWrite) in place of the retired stamp.content/docs/automation/email-templates.mdxsays a template is changed in Studio and the rows are closed to organization writes.scripts/audits/14744-before-update-per-row-value-probe.mjs: this audit script imported the retired module. Its plugin-email subject and that import are dropped, which is all it needed. The probe still runs (probe: 7 subjects). The dated audit recorddocs/audits/2026-09-multi-update-per-row-value-census.mdstays as measured.platform-objects(cross-lane, its own commitbffc546129): thesys_email_templatefield help foris_system("tenants may edit") andcustomized("set when an admin edits") became false with this change. Both are corrected in en, zh-CN, ja-JP and es-ES.node scripts/check-i18n-bundles.mjs --writeregenerated the en bundle. See Acceptance notes.@objectstack/plugin-emailand@objectstack/plugin-authtakeminorwith the BREAKING banner, and@objectstack/platform-objectstakespatch. The ADR-0087 disposition isnot-required (no-migration-prescription), and each retired export is named.Pins, each negative one ablated and restored by blob
The SMS legs were re-run at head
a4ba9d5070and the door legs at1c2a3ad64b(unit) anda93579f453(dogfood, through dist); neither the door's source nor its tests have changed since. Every leg usedscripts/ablation-replace.mjs, which proved that the mutation landed on disk and that the restore matched the HEAD blob with an emptygit diff HEAD. Every driver carries an EXIT/INT/TERM trap that restores from HEAD.PERMISSION_DENIED, 403, door named); nothing reaches the driverplugin-email/src/email-template-door.test.tsPATCHandPOST /api/v1/data/sys_email_templateas the org admin)email-template-overlay-survives-boot.dogfood.test.ts, case 3ablation-dist-preflightfound the marker in 2 dist filesPATCHanswered 200), cases 1 and 2 green; after restore and rebuild,--absentpassed, the tree was clean, and all 3 were greencustomizedemail-template-door.test.ts§3sys_notification_templaterowplugin-auth/src/phone-sms-seed-retired.test.tskernel:readydataservice AuthManager writes through; fixed ina93579f453, then re-ablatedNot ablated, positive pins: a
singleStudio save still reaches the mail and survives a cold boot. That is dogfood cases 1 and 2, unchanged and green.Tests
Every result below is at head
a4ba9d5070. Each exit code was captured before any pipe.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives on this change. Reconciled with--ran(exit codes recorded):123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN.check-tenant-audit-census(and its self-test): the retired seed's write site leaves the census at 232.node scripts/tenant-audit-census.mjs --writeregenerated the census page and the counts file, and the page's hand-written figures follow it.check:engine-double-contractandcheck:where-matcher: the new SMS test doubles now conform. The ledgerscripts/engine-double-contract.pinned.jsonwas updated bynode scripts/check-engine-double-contract.mjs --write.check:dual-build-cjs-loadsandspec check:skill-examplesfirst answered PREREQUISITE NOT MET (exit 3).@objectstack/plugin-emailtest: 535 passed. typecheck: OK.@objectstack/plugin-authtest: 2616 passed, 10 skipped. typecheck: OK.@objectstack/platform-objectstest: 1006 passed. typecheck: OK.turbo build --filter=@objectstack/dogfood^..., 63/63).email-template-overlay-survives-boot.dogfood.test.ts,email-template-materialization.dogfood.test.tsandemail-template-boot-sweep.test.tspassed 8 of 8. These are the dogfood files that bootplugin-email; none sends SMS.examples/app-showcase/test/email-template-locale.test.tspassed 6 of 6.npx eslint --no-inline-config --format json. None was reported as ignored by the config.eslint.config.mjsenables no type-aware linting and no import-resolution rule, so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintis CI's.维护者速读(草稿)
sys_email_template),数据接口会返回 403 并说明这扇门已关;平台自己的写入(内置模板、声明模板、Studio 保存后的同步)照常。随之退役"已自定义"标记钩子及其三个导出。手机短信验证码/邀请的内置文案不再在启动时写成sys_notification_template行,而是按语言逐级取:有运营自建的行就用行,没有就用内置文案,发出去的短信逐字节不变。ADR-0131 §6 Q1 下补一条日期注记,记录你 10 月 6 日的裁决 C。sys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206),否则单租户下 Studio 改模板会失效(已实测)。platform-objects那一处字段说明的跨车道修正可以随本 PR 一起落地。Acceptance notes
email-template-overlay-survives-boot.dogfood.test.tsstay green and unchanged. Its first two cases (thesingleStudio edit path) are byte-unchanged and green. Its third case, "a data-door edit is stamped customized and survives the next cold boot", pinned exactly the behaviour this PR retires. Measured unchanged on this branch, it went red with the new refusal (expected 403 to be 200). Per the original order's rule ("a dogfood file that pins the retired behaviour is updated in this PR"), case 3 now pins the closed door: an edit and a create are refused with 403PERMISSION_DENIEDand the door named, nothing is markedcustomized, and the metadata-door wording survives the next cold boot.platform-objectshelp-text fix sits indomain:engine's package, outside the claim's file surface. It is made because this change made the two texts false. It lives in one separate commit (bffc546129), so the seat can declare it or drop it.bootstrap-declared-email-templates.tsstill names the retired stamp in a@link. That is a comment only, but that file is outside stage 1.packages/spec/liveness/email_template.json's_notenarrates the stamp; it is narrative, not an evidence anchor,check:livenessdoes not read it, andpackages/specis untouched here.Generated by Claude Code