Repository navigation
Commit 09720e2
fix(auth): say on /auth/config whether email leaves the deployment, so forgot-password can stop claiming a link was sent (#30365)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6s33jXCmdvuMRxu2PeJhbi)_
domain:server#2 · os-bill · session_01Utp6CbSX6BdpVNjS79LB5A
Fixes #30107
Clause-②: yes (widening) — `GET /api/v1/auth/config` gains an optional
`features.emailDelivery` boolean (`AuthFeaturesConfigSchema` in
`@objectstack/spec/api`), and the flag joins `PUBLIC_AUTH_FEATURES`, so
`requiresFeature` on an action accepts one more value,
`'emailDelivery'`.
Metadata-protocol: added — that `requiresFeature` value is a new
accepted value an author can write; the Claim's `none` was re-measured:
the PR moves `packages/spec/authorable-surface/api.json` (a `none`
declaration is refused for that by the level axis) and widens the
`requiresFeature` enum.
Skills-docs: none — no skill or hand-written doc enumerates the
`requiresFeature` values or the `/auth/config` feature flags; the
generated reference pages
(`content/docs/references/api/auth-endpoints.mdx`,
`content/docs/references/ui/action.mdx`) carry the new member and value
in this PR.
## 维护者速读
- 改了什么:登录配置接口 `GET /api/v1/auth/config` 新增一个开关
`features.emailDelivery`,说明「这台服务器能不能真的发出邮件」。只有接了邮件服务且该服务自报已配置时为
true;没有邮件服务、或用默认的 log 传输(只把邮件打印到服务器控制台)时为 false。忘记密码页据此就能不再谎称「重置链接已发送」。
-
为什么改:新装的自托管部署默认不发邮件,成员点忘记密码却看到「已将重置链接发送至你的邮箱」,等一封永远不会到的信;服务端其实知道发不出去,只写进日志,页面无从得知。
-
风险与回滚:只加一个可选字段,现有字段与行为不变。它说的是部署、不是某个邮箱:已注册和未注册的邮箱拿到的重置请求应答仍完全相同,不会泄露账号是否存在。判定复用邀请成员已在用的同一条规则(`isEmailDeliveryLive`),每次请求现读,邮件服务后配上也不用重启。回滚即撤本
PR。
- 先看:`packages/plugins/plugin-auth/src/auth-manager.ts` 的
`getPublicConfig()` 里 `emailDelivery`
一行;`packages/spec/src/kernel/public-auth-features.ts` 的 `emailDelivery`
条目;`packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts`
新增的一组用例。
- 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run
src/reset-request-mail-after-response.test.ts`,预期 1 个文件、12 条全绿。
- 需要你做的:无,席位落地;页面文案是 `domain:ui` 的另一半,由席位立卡。
## What changes
**The rule: the public auth config says whether mail leaves this
deployment, and says it about the deployment, never about an address.**
- **The served flag (`@objectstack/plugin-auth`).** `getPublicConfig()`
adds `features.emailDelivery: this.isEmailDeliveryLive()`. That is the
one deliverability rule this package already has: the wired email
service's own `isConfigured()`, read per call, the rule sign-up and
invite-member's `outcome: 'sent' | 'link_only'` already use. It is not
`hasEmailTransport()` / `isEmailServiceAvailable()`, which answer "a
service object is wired" and are true on every default boot, whose `log`
transport writes mail to the console. So: no email service, `false`; the
`log` transport (or any service reporting itself unconfigured, or one
without the member), `false`; a configured transport, `true`. Read at
request time, so a transport configured later from the mail settings is
reported without a restart. The hono adapter's `/auth/config` arm serves
`getPublicConfig()` unchanged, so it serves the flag too.
- **The contract (`@objectstack/spec`).** `AuthFeaturesConfigSchema`
gains `emailDelivery: z.boolean().optional()`, and
`PUBLIC_AUTH_FEATURES` classifies it (`surface: 'login'`, `semantics:
'opt-in'`, exempt: no spec input is gated), which the drift guard in
plugin-auth requires for every boolean the config serves. Being
classified also makes it a `requiresFeature` value, lowered to
`features.emailDelivery == true`.
- **Why this carrier and not an `outcome` on the reset answer.**
Measured both. The `/auth/config` flag has the exact precedent in
`features.phoneNumberOtp` ("only advertised when an SMS service can
actually deliver"), which already drives the phone branch of the same
forgot-password form; the page reads the config before submit, so it can
say "a reset link cannot be emailed here, ask an administrator" on the
form itself instead of after a pointless submit; and the console's
`forgotPassword(email)` resolves to `void` and discards the reset
answer, so an answer member would need more console plumbing. The reset
answer itself (`POST /api/v1/auth/request-password-reset`, `{ status:
true, message }`) is not changed.
- **No account-existence oracle.** The flag is a property of the
deployment and is the same for every caller. The reset request still
answers a known and an unknown address with the same status and body,
with and without mail; the new pins assert that in all three transport
states.
## Tests
`packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts`,
a new top-level `describe` in the reset door's existing harness (real
`AuthManager.handleRequest` over a real `ObjectQL` engine on
better-sqlite3), 3 cases:
- no email service: `features.emailDelivery` is `false`, and the reset
request for an address with an account and one without answers the same
`200` body;
- a transport reporting itself unconfigured (the `log` shape): `false`,
same answer for both addresses;
- a configured transport: `true`, both addresses answer the same, and
that answer equals the one a deployment without mail gives.
Updated: the exact `features` fixture in `auth-manager.test.ts`
(`emailDelivery: false`, no email service there), the served fixture in
`packages/spec/src/api/auth-endpoints.test.ts`, and the registry roster
in `packages/spec/src/kernel/public-auth-features.test.ts` (12 → 13
flags).
<details><summary>Ablations (forbidden behaviour put back with
`scripts/ablation-replace.mjs`, red seen, restored; the tests import
`./auth-manager.js` from source)</summary>
- A. `emailDelivery: this.isEmailServiceAvailable()` (the "service
wired" reader): `Tests 1 failed | 11 passed (12)` — the log-transport
case, `AssertionError: expected true to be false`. Restore: blob
`5bc1cba772b0…` equals the HEAD blob.
- B. the `emailDelivery` line deleted: `Tests 5 failed | 12 passed (17)`
over the reset harness and `public-feature-registry.test.ts` — all three
new cases, plus the registry drift guard ("every boolean flag served …
is classified") and its default-semantics case. Restore: blob equals the
HEAD blob.
</details>
## Acceptance notes
- **The console half (`domain:ui`), not in this PR.**
`ui/packages/auth/src/ForgotPasswordForm.tsx` and the
`successDescription` strings in `ui/packages/i18n` still say the link
was sent; the form should read `features.emailDelivery` from
`/auth/config` (as it reads `phoneNumberOtp`) and, when it is not
`true`, say a reset link cannot be emailed on this deployment and to ask
an administrator (who can set a password from Setup → Users), the same
for every address. Named for the seat to file; the UI lane is off duty.
- **Siblings measured.** The visitor verification-email resend already
refuses with `400 EMAIL_SERVICE_REQUIRED` when the transport is not live
(`visitor-verification-resend.ts`), so it does not say "sent".
Invite-member already answers `outcome: 'link_only'`. `POST
/magic-link/send` still answers success with no mail, but magic link is
not advertised on `/auth/config` and no console UI calls it; a caller
can now read `features.emailDelivery` first. Noted, not filed.
## Verification
Gates, suites and lint on head `f78457d8` (`origin/main` merged in at
that commit).
<details><summary>Suites and typecheck</summary>
- `pnpm --filter @objectstack/spec test`: Test Files 813 passed (813),
Tests 25579 passed | 1 todo (25580).
- `pnpm --filter @objectstack/plugin-auth test`: Test Files 220 passed
(220), Tests 3879 passed | 10 skipped (3889).
- `pnpm --filter @objectstack/platform-objects exec vitest run
src/feature-gate-guard.test.ts` (the registry's other consumer): 112
passed.
- `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter
@objectstack/plugin-auth typecheck`: exit 0.
- `pnpm --filter @objectstack/spec check:generated`:
`content/docs/references/**` stale after the `.describe()` change;
`--fix` regenerated exactly that one (the `auth-endpoints` and `action`
reference pages, the latter because `requiresFeature`'s enum grew),
committed.
</details>
<details><summary>Derived gates: 106 derived, 105 exit 0, 1 NOT
MEASURED</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 106 commands; each ran with its exit
code captured before any pipe.
- `--ran` reconciliation: "106 derived famil(ies) accounted for — 105
run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` exits 3 (PREREQUISITE
NOT MET, 35 packages without `dist/`; it needs a whole-workspace build).
CI runs it.
- Re-run after their prerequisite: `node
scripts/check-plugin-teardown-shape.mjs --self-test` exit 0 ("48 cases
pass", after `git fetch origin 621a487…` on the shallow checkout);
`check:skill-examples` exit 0 after building `@objectstack/client` and
`client-react` ("283 prose examples type-check").
- `pnpm lint` (`eslint . --no-inline-config`): exit 0.
- No full dogfood: the title carries no `!` and the Clause-② arm is
`(widening)`.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [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.
- [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 (`authorable-surface/api.json`, and the two
auto-generated reference pages from `check:generated --fix`).
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/` (the two reference
pages are generator output).
- [x] 8. Title, body and comments are in English (the 维护者速读 block is
Chinese by the Communication rule).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A
<!-- signed: domain:server#2 · os-bill ·
session_01Utp6Cbvcmv8YWFhFJwzjKL -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d3f23d2 commit 09720e2
11 files changed
Lines changed: 101 additions & 6 deletions
File tree
- .changeset
- content/docs/references
- api
- ui
- packages
- plugins/plugin-auth/src
- spec
- authorable-surface
- src
- api
- kernel
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| |||
200 | 201 | | |
201 | 202 | | |
202 | 203 | | |
| 204 | + | |
203 | 205 | | |
204 | 206 | | |
205 | 207 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
| |||
248 | 249 | | |
249 | 250 | | |
250 | 251 | | |
251 | | - | |
| 252 | + | |
252 | 253 | | |
253 | 254 | | |
254 | 255 | | |
| |||
316 | 317 | | |
317 | 318 | | |
318 | 319 | | |
| 320 | + | |
319 | 321 | | |
320 | 322 | | |
321 | 323 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3208 | 3208 | | |
3209 | 3209 | | |
3210 | 3210 | | |
| 3211 | + | |
| 3212 | + | |
3211 | 3213 | | |
3212 | 3214 | | |
3213 | 3215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2790 | 2790 | | |
2791 | 2791 | | |
2792 | 2792 | | |
2793 | | - | |
| 2793 | + | |
| 2794 | + | |
| 2795 | + | |
2794 | 2796 | | |
2795 | 2797 | | |
2796 | 2798 | | |
| |||
4701 | 4703 | | |
4702 | 4704 | | |
4703 | 4705 | | |
| 4706 | + | |
| 4707 | + | |
| 4708 | + | |
| 4709 | + | |
| 4710 | + | |
| 4711 | + | |
| 4712 | + | |
| 4713 | + | |
4704 | 4714 | | |
4705 | 4715 | | |
4706 | 4716 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
32 | 38 | | |
33 | 39 | | |
34 | 40 | | |
| |||
297 | 303 | | |
298 | 304 | | |
299 | 305 | | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
300 | 345 | | |
301 | 346 | | |
302 | 347 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| 217 | + | |
217 | 218 | | |
218 | 219 | | |
219 | 220 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
234 | 234 | | |
235 | 235 | | |
236 | 236 | | |
| 237 | + | |
237 | 238 | | |
238 | 239 | | |
239 | 240 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
262 | 262 | | |
263 | 263 | | |
264 | 264 | | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
265 | 270 | | |
266 | 271 | | |
267 | 272 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
20 | | - | |
| 19 | + | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
0 commit comments