Repository navigation
fix(core, plugin-auth): a failed organization probe reads as readFailed, and an auth mail treats a failed currency read as transient - #28088
Conversation
…ed, and an auth mail retries a failed currency read The localization resolver's sys_organization probe, which decides whose sys_setting rows a caller naming no organization may read, now answers readFailed: true on an AuthzStoreUnavailableError, with no memo of either kind. The auth mails' tenant currency reader rejects on readFailed, and a render refusal that only the missing default caused becomes a transient error that names the read instead of TEMPLATE_INVALID. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 49 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 9274bc7f848902c588b1d8bcf7f3c7632d7c9145 && git checkout 9274bc7f848902c588b1d8bcf7f3c7632d7c9145
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ec7e15abcb66afd638932b68e0ce4bc1b17bb313 eb1c3932176dc9aab7cce58b1558289d706e0640 && git checkout -B drift-repro ec7e15abcb66afd638932b68e0ce4bc1b17bb313 && git merge --no-ff eb1c3932176dc9aab7cce58b1558289d706e0640
node scripts/docs-audit/affected-docs.mjs --json ec7e15abcb66afd638932b68e0ce4bc1b17bb313
|
|
domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia Seat ACCEPT · PR #28088 · head
|
Requested by Elon · project thread
domain:server#3 · os-elon-musk · session_016WV4czs2fGS6UiDUUbdGrf
Fixes #27733
Fixes #27710
Clause-②: no
Metadata-protocol: none
维护者速读
resolveLocalizationContext在调用方未指明组织、且组织探针(sys_organization)因存储故障抛AuthzStoreUnavailableError时,也返回readFailed: true(与 fix(core, service-messaging): a failed settings read is retried, not dead-lettered as TEMPLATE_INVALID #27730 的设置读失败同义),且这种结果两种缓存都不写,下一次请求重新探。plugin-auth 的认证邮件币种读取在readFailed时拒绝;若渲染因此只缺默认币种而被拒,先以XXX试渲染一次(不发送),能过就抛一条点名"币种读取失败、读成功后可再发"的瞬时错误,不再是TEMPLATE_INVALID加"去设置 localization.currency"的误导性补救。readFailed已存在,只多一个触发条件;新函数不在包入口导出),Clause-②: no。探针故障时不再缓存,故障期间每次请求多一次探针查询。无法解析的 posture(配置错误)不算存储故障,行为不变。回滚即 revert。packages/core/src/security/resolve-authz-context.ts的localizationFallbackReach与resolveLocalizationContext中probeFailed分支;packages/plugins/plugin-auth/src/mail-tenant-currency.ts的refusalAfterCurrencyReadFault。pnpm --filter @objectstack/plugin-auth exec vitest run src/mail-tenant-currency.test.ts,预期 17 条全过(其中 10 条为本 PR 新增:读失败两条腿判瞬时、读成功无默认原样拒绝、无法用默认治愈的拒绝原样保留)。plugin-authandservice-automationread a failed tenant-settings read as "no default", like #27257; adoptreadFailedonce it lands (automation fails thenotifyrun) #27710 的 service-automation 一腿需要动热文件engine.ts的解析器契约,已移交 service-automation: a failed tenant-settings read fails thenotifyrun as a permanent guard refusal; the resolver contract and run setup cannot carryreadFailed#28056,本 PR 不含。What changes
Core: the organization probe is a read too (#27733). For a caller that names no organization where no wall stands, the direct
sys_settingfallback first askssys_organization(throughresolveGuestOrganization) whose rows it may read.localizationFallbackReachturned a throw there intounowned, so nosys_settingread ran and the answer had noreadFailed. It now returns{ reach, probeFailed }, whereprobeFailedisisAuthzStoreUnavailableError(err), andresolveLocalizationContextsetsreadFailed: trueon it. Per triage 6509207804, such an answer writes no failure memo; it is not stored in the success cache either (it holds no value to keep), and the dead entry it supersedes is dropped, so the next request probes again. Any other throw (anOS_TENANCY_POSTUREthat cannot be read, a configuration error, not transient) still answersunownedwith noreadFailed. The settings-read memo and the settings-service legs are unchanged. ThereadFaileddoc comment now names both triggers.plugin-auth: the auth mails' tenant currency (#27710).
withMailTenantCurrency(mail-tenant-currency.ts) wraps the email service the plugin hands the AuthManager. Its resolver used to return.currencyfor every answer, so a failed read added nocurrencyand the render refused a no-argument{{ amount | currency }}holeTEMPLATE_INVALIDwith the "set localization.currency" remedy. Now:auth-plugin.tstolocalizationMailCurrencyResolver(same file as the wrapper, not exported from the package entry). It rejects whenreadFailedis set, as messaging'sgetTenantCurrencydoes since fix(core, service-messaging): a failed settings read is retried, not dead-lettered as TEMPLATE_INVALID #27730.TEMPLATE_INVALID, it renders the template once more without sending (renderTemplate, with the ISO no-currency codeXXX). If that still refusesTEMPLATE_INVALID, a default cannot cure the template (an@pathvalue that is not a code) and that refusal stands. If it renders, or the service has norenderTemplate, the mail fails withthe mail '<template>' was not rendered: the organization's default currency could not be read (<cause>); it can be sent once that read succeeds, which carries noTEMPLATE_*code. The auth mail has no retry ladder of its own: the caller (a resend endpoint, a background send) sees a transient error it can retry, instead of a permanent one.service-automation: not in this PR. #27710's notify-run leg moved to #28056 (seat 3).
plugin.tsalone cannot express it: the resolver's rejection and itsundefinedare swallowed alike inrun-resume.ts(run setup andwithTenantCurrency); the resolver contractFlowTenantCurrencyResolverthat says "throws, tolerated" is documented inengine.ts, a held hot file (#27951); and the missing-default refusal is aFlowTextTemplateErrorbranded a guard refusal inbuiltin/template.ts, with noAutomationContextmember (apackages/speccontract) to carry "the read failed" to it.Why
Triage 6509207804 (#27733) and 6506866652 (#27257 retriage, which graded
readFailedand filed #27710): a failed read learned nothing and is transient; only a read that succeeded and found no default is permanent. #27730 covered thesys_settingleg and messaging; this covers the probe leg one step earlier and the plugin-auth caller.Tests
packages/core/src/security/resolve-localization-read-failed.test.ts(5 new): a store fault on the probe answers the built-ins withreadFailed: trueand reads nosys_setting; no memo of either kind (an engine with the write-epoch and middleware seams, so a success entry would be kept if written): the next request probes again, and once the store answers it readsCNYwith noreadFailed; a probe that succeeds and finds no organization has noreadFailed; an unreadable posture has none; a caller naming its organization is not probed.packages/services/service-messaging/src/template-tenant-currency.test.ts(1 new, the email channel throughMessagingServicePlugin's own wiring): a delivery with no organization whose probe fails renders withoutcurrency, trial-renders withXXX, sends nothing, and is gradedretryablewith noTEMPLATE_text; once the probe answers, the next delivery formats inCNY.packages/plugins/plugin-auth/src/mail-tenant-currency.test.ts(10 new): with a failed read, a missing-default refusal becomes the transient error (exact text, noTEMPLATE_, oneXXXtrial, nothing sent); a service with norenderTemplategets it too; an@pathrefusal, alone or behind the missing default, stands;TEMPLATE_NOT_FOUNDstands with no trial; a template needing no default is sent; a successful read with no default keeps the render's refusal with no trial. ThroughlocalizationMailCurrencyResolverover the realresolveLocalizationContext: a failedsys_settingread and a failedsys_organizationprobe (identity mail) both reach the mail as the transient error; a read with no default reaches it as the refusal; a read that findsCNYformats in it.Acceptance notes
main@bc496bf7:localizationFallbackReachstill mapped the probe's throw toUNOWNED_ORGANIZATION_REACHwith noreadFailed;auth-plugin.tsandservice-automation/src/plugin.tsstill returned.currencywithout readingreadFailed. Both premises held.readFailed?: truealready exists);localizationMailCurrencyResolverlives inplugin-auth/src/mail-tenant-currency.ts, which no package entry re-exports. The contract review is therefore not expected, as the dispatch said.@objectstack/coreand@objectstack/plugin-auth,patch.service-messagingmoved no code (one new test), so it has none;service-automationhas no change here.git grep -n "readFailed\|no tenant default\|could not be read" -- skills docs contentnames no text about this resolver or these mails.content/docs/automation/email-templates.mdxandcontent/docs/kernel/runtime-services/email-service.mdxdescribe only the "no default currency reaches the render" case, which is unchanged.claude/project-thread-abzzymnames no issue; claims 6520779833 / 6520770423 (amended) are the identity record.plugin-auth/src.Verification
Head
eb1c3932(one commit onmain@bc496bf7).scripts/ablation-replace.mjs; each leg red, then restored with blob == HEAD):probeFailed: false): core 2 of 10 red; with core rebuilt, messaging 1 red (the probe pin) and plugin-auth 1 red (the end-to-end probe pin).probeFailed: true): core 1 red (the unreadable-posture pin).throw err): plugin-auth 5 red.@pathpins).@objectstack/core128 files / 2878 passed;@objectstack/plugin-auth204 / 3896 passed (10 skipped);@objectstack/service-messaging62 / 756 passed.typecheckexit 0 forcore,plugin-auth,service-messaging(the first messaging run caught an ES2022Array.prototype.atin the new pin; replaced by an index).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 69 families, after a whole-treeturbo run build. 68 exited 0 on the first run;check:objectql-double-limitflagged the new plugin-authfinddouble as unjudged, and after rewriting it on the generic where-matcher it exits 0. That gate and the eight others that read test files (where-matcher,engine-double-contract,cross-package-test-inputs,test-source-alias,nul-bytes,issue-citations,undeclared-dep-imports,query-options-erasure) were rerun ateb1c3932, all exit 0; the derived list ateb1c3932is the same 69.--ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lintexit 0; ESLint on the two test files changed afterwards, 0 errors.!and there is no(narrowing)arm.Human Minimum
packages/specholds no runtime logic. (packages/specuntouched.)pnpm lintpasses.main, one pull request. (Two sibling cards, one dispatch.)packages/spectouched: its generated artifacts are regenerated and committed. (Not touched.)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf