You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9cbe507
Browse filesBrowse the repository at this point in the historyBrowse files
fix(core, plugin-auth): a failed organization probe reads as readFailed, and an auth mail treats a failed currency read as transient (#28088)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DDUsM7i5cZHXRCUW51mVsp4)_
domain:server#3 · os-elon-musk · session_016WV4czs2fGS6UiDUUbdGrf
Fixes#27733Fixes#27710
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`resolveLocalizationContext`
在调用方未指明组织、且组织探针(`sys_organization`)因存储故障抛 `AuthzStoreUnavailableError`
时,也返回 `readFailed: true`(与 #27730
的设置读失败同义),且这种结果两种缓存都不写,下一次请求重新探。plugin-auth 的认证邮件币种读取在 `readFailed`
时拒绝;若渲染因此只缺默认币种而被拒,先以 `XXX` 试渲染一次(不发送),能过就抛一条点名"币种读取失败、读成功后可再发"的瞬时错误,不再是
`TEMPLATE_INVALID` 加"去设置 localization.currency"的误导性补救。
- 为什么改:探针故障时原先读成"没有组织 → 没有默认",消息投递被判永久进死信;认证邮件把一切读失败都当"没有默认币种"。分诊
6509207804、6506866652:读失败是瞬时的,只有读成功且无默认才是永久的。
- 风险与回滚:不改任何公开类型(`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
新增:读失败两条腿判瞬时、读成功无默认原样拒绝、无法用默认治愈的拒绝原样保留)。
- 需要你做的:无。#27710 的 service-automation 一腿需要动热文件 `engine.ts` 的解析器契约,已移交
#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_setting` fallback first asks `sys_organization` (through
`resolveGuestOrganization`) whose rows it may read.
`localizationFallbackReach` turned a throw there into `unowned`, so no
`sys_setting` read ran and the answer had no `readFailed`. It now
returns `{ reach, probeFailed }`, where `probeFailed` is
`isAuthzStoreUnavailableError(err)`, and `resolveLocalizationContext`
sets `readFailed: true` on 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 (an
`OS_TENANCY_POSTURE` that cannot be read, a configuration error, not
transient) still answers `unowned` with no `readFailed`. The
settings-read memo and the settings-service legs are unchanged. The
`readFailed` doc 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
`.currency` for every answer, so a failed read added no `currency` and
the render refused a no-argument `{{ amount | currency }}` hole
`TEMPLATE_INVALID` with the "set localization.currency" remedy. Now:
- The resolver moved from an inline closure in `auth-plugin.ts` to
`localizationMailCurrencyResolver` (same file as the wrapper, not
exported from the package entry). It rejects when `readFailed` is set,
as messaging's `getTenantCurrency` does since #27730.
- On a rejected read the wrapper sends without a currency. When that
send throws `TEMPLATE_INVALID`, it renders the template once more
without sending (`renderTemplate`, with the ISO no-currency code `XXX`).
If that still refuses `TEMPLATE_INVALID`, a default cannot cure the
template (an `@path` value that is not a code) and that refusal stands.
If it renders, or the service has no `renderTemplate`, the mail fails
with `the 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 no `TEMPLATE_*` 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.
- Every other failure, a successful read with no default, and a template
with no such hole are unchanged.
**service-automation: not in this PR.** #27710's notify-run leg moved to
#28056 (seat 3). `plugin.ts` alone cannot express it: the resolver's
rejection and its `undefined` are swallowed alike in `run-resume.ts`
(run setup and `withTenantCurrency`); the resolver contract
`FlowTenantCurrencyResolver` that says "throws, tolerated" is documented
in `engine.ts`, a held hot file (#27951); and the missing-default
refusal is a `FlowTextTemplateError` branded a guard refusal in
`builtin/template.ts`, with no `AutomationContext` member (a
`packages/spec` contract) to carry "the read failed" to it.
## Why
Triage 6509207804 (#27733) and 6506866652 (#27257 retriage, which graded
`readFailed` and filed #27710): a failed read learned nothing and is
transient; only a read that succeeded and found no default is permanent.
#27730 covered the `sys_setting` leg 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 with
`readFailed: true` and reads no `sys_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 reads `CNY` with no `readFailed`; a probe that succeeds
and finds no organization has no `readFailed`; 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 through `MessagingServicePlugin`'s own
wiring): a delivery with no organization whose probe fails renders
without `currency`, trial-renders with `XXX`, sends nothing, and is
graded `retryable` with no `TEMPLATE_` text; once the probe answers, the
next delivery formats in `CNY`.
- `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, no `TEMPLATE_`, one `XXX` trial, nothing
sent); a service with no `renderTemplate` gets it too; an `@path`
refusal, alone or behind the missing default, stands;
`TEMPLATE_NOT_FOUND` stands with no trial; a template needing no default
is sent; a successful read with no default keeps the render's refusal
with no trial. Through `localizationMailCurrencyResolver` over the real
`resolveLocalizationContext`: a failed `sys_setting` read and a failed
`sys_organization` probe (identity mail) both reach the mail as the
transient error; a read with no default reaches it as the refusal; a
read that finds `CNY` formats in it.
## Acceptance notes
- **Re-measured on `main` @ `bc496bf7`:** `localizationFallbackReach`
still mapped the probe's throw to `UNOWNED_ORGANIZATION_REACH` with no
`readFailed`; `auth-plugin.ts` and `service-automation/src/plugin.ts`
still returned `.currency` without reading `readFailed`. Both premises
held.
- **Clause-②: no**, confirmed from the diff: no exported type changes
(`readFailed?: true` already exists); `localizationMailCurrencyResolver`
lives in `plugin-auth/src/mail-tenant-currency.ts`, which no package
entry re-exports. The contract review is therefore not expected, as the
dispatch said.
- **Changeset:** `@objectstack/core` and `@objectstack/plugin-auth`,
`patch`. `service-messaging` moved no code (one new test), so it has
none; `service-automation` has no change here.
- **Skills-docs:** none. `git grep -n "readFailed\|no tenant
default\|could not be read" -- skills docs content` names no text about
this resolver or these mails.
`content/docs/automation/email-templates.mdx` and
`content/docs/kernel/runtime-services/email-service.mdx` describe only
the "no default currency reaches the render" case, which is unchanged.
- **Branch name:** the card thread's harness-pinned branch
`claude/project-thread-abzzym` names no issue; claims 6520779833 /
6520770423 (amended) are the identity record.
- **Open PRs:** none of the 20 open PRs touches these files. #27862
(seat 3) edits other hunks of `plugin-auth/src`.
## Verification
**Head `eb1c3932`** (one commit on `main` @ `bc496bf7`).
- **Ablations** (`scripts/ablation-replace.mjs`; each leg red, then
restored with blob == HEAD):
- A1, the probe fault sets no kind (`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).
- A2, the no-memo branch skipped (the answer falls into the success
cache): core 1 red (the no-memo pin).
- A3, every derivation throw counts (`probeFailed: true`): core 1 red
(the unreadable-posture pin).
- A4, the auth resolver never rejects: plugin-auth 2 red (both
end-to-end read-fault pins); rerun after the last fixture change, still
2 red.
- A5, the wrapper never re-grades (`throw err`): plugin-auth 5 red.
- A6, the trial render's standing refusal ignored: plugin-auth 2 red
(both `@path` pins).
- **Tests:** `@objectstack/core` 128 files / 2878 passed;
`@objectstack/plugin-auth` 204 / 3896 passed (10 skipped);
`@objectstack/service-messaging` 62 / 756 passed.
- **Types:** `typecheck` exit 0 for `core`, `plugin-auth`,
`service-messaging` (the first messaging run caught an ES2022
`Array.prototype.at` in the new pin; replaced by an index).
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 69 families, after a whole-tree
`turbo run build`. 68 exited 0 on the first run;
`check:objectql-double-limit` flagged the new plugin-auth `find` double
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 at
`eb1c3932`, all exit 0; the derived list at `eb1c3932` is the same 69.
`--ran`: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
- **Lint:** `pnpm lint` exit 0; ESLint on the two test files changed
afterwards, 0 errors.
- **Dogfood battery:** not run; the title has no `!` and there is no
`(narrowing)` arm.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` untouched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request. (Two sibling cards, one dispatch.)
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4czs2fGS6UiDUUbdGrf
Co-authored-by: Claude <noreply@anthropic.com>
fix(core, plugin-auth): a failed organization probe says `readFailed` too, and an auth mail no longer reads a failed settings read as "no default currency" (#27733, #27710)
7
+
8
+
Clause-②: no — `resolveLocalizationContext`'s result keeps its shape; the existing `readFailed?: true` member gains one more trigger. Nothing new is exported from a package entry.
9
+
Metadata-protocol: none
10
+
11
+
-**Before.** For a caller that names no organization where no wall stands, `resolveLocalizationContext` first asks `sys_organization` which organization's rows it may read. A store fault on that probe answered the built-ins with no `readFailed`, so messaging graded a mail whose `{{ amount | currency }}` hole then had no default as permanent (`TEMPLATE_INVALID`) and dead-lettered it. And the auth mails (verification, password reset, invitations, magic link, notices) read every failed settings read as "the organization has no default currency": such a mail failed with `TEMPLATE_INVALID` and its "set localization.currency" remedy during a transient fault.
12
+
-**Now.** A probe that throws `AuthzStoreUnavailableError` answers `readFailed: true`, the same as a failed `sys_setting` read, so messaging retries the delivery. That answer is cached in neither memo, since no value was read: the next request asks again. A posture that cannot be read is not a store fault and still answers no `readFailed`. The auth mail's currency read rejects on `readFailed`; when the render then refuses for want of a default, the mail is rendered once more without sending with the no-currency code `XXX`: if that renders, the mail fails with an error that names the failed read and carries no `TEMPLATE_*` code, so sending it again once the read succeeds works; if it still refuses, that refusal stands.
13
+
-**Unchanged.** A read that succeeds and finds no default keeps today's refusal and remedy, on both paths. A template with no no-argument `currency` hole is sent during a read fault. The failure memo for a failed `sys_setting` read is unchanged.
0 commit comments