Repository navigation
fix(auth): say on /auth/config whether email leaves the deployment, so forgot-password can stop claiming a link was sent - #30365
Conversation
GET /api/v1/auth/config gains features.emailDelivery, served from the one deliverability rule (isEmailDeliveryLive: the wired email service's own isConfigured()). False with no email service and with the stock log transport; true with a configured one. The forgot-password page can read it instead of saying a reset link was sent where no mail leaves. The flag describes the deployment, not an address: the reset request still answers known and unknown addresses the same way. Claude-Session: https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A Co-authored-by: Claude <noreply@anthropic.com>
…snapshot Claude-Session: https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A Co-authored-by: Claude <noreply@anthropic.com>
The auto-generated auth-endpoints and action references, from check:generated --fix. The changeset declares the requiresFeature value the registry entry adds. Claude-Session: https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 5 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 — 175 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 eeddf619b4a05f43c1f921ee118bfc08a5890ae4 && git checkout eeddf619b4a05f43c1f921ee118bfc08a5890ae4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a a211f203b852238b9b79a100125139e877f0f142 && git checkout -B drift-repro fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a && git merge --no-ff a211f203b852238b9b79a100125139e877f0f142
node scripts/docs-audit/affected-docs.mjs --json fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a
|
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: Inputs read: card 30107 body and all three comments (6633840753 triage, 6651778079 claim, 6652972753 os-dev-report); PR 30365 body, 11-file list, net diff against origin/main (fdad178) at this head; the head's check-runs, polled 08:07Z, 08:12Z, 08:13Z and at the time stamped in the check-runs paragraph below. Verdict independent of the dispatch order and of the report's conclusions. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: FAIL |
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL Contract reviewServed-tier: Re-review of the same head after the PR-body edit that closes M1 of record 6653160903. The tree is byte-identical: the PR's only commit is still ① Derived judgmentsUnchanged from 6653160903: ② Semver level
③ Boundary flags
④ Design judgmentUnchanged from 6653160903, nothing in the tree moved: the account-existence oracle holds (known and unknown addresses pinned byte-identical in all three transport states, send deferred past the answer on the public door); Implemented-by: VERDICT: PASS |
|
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL 维护者速读
席位核对 — ACCEPT · domain:server#2 · head
|
Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6CbSX6BdpVNjS79LB5A
Fixes #30107
Clause-②: yes (widening) —
GET /api/v1/auth/configgains an optionalfeatures.emailDeliveryboolean (AuthFeaturesConfigSchemain@objectstack/spec/api), and the flag joinsPUBLIC_AUTH_FEATURES, sorequiresFeatureon an action accepts one more value,'emailDelivery'.Metadata-protocol: added — that
requiresFeaturevalue is a new accepted value an author can write; the Claim'snonewas re-measured: the PR movespackages/spec/authorable-surface/api.json(anonedeclaration is refused for that by the level axis) and widens therequiresFeatureenum.Skills-docs: none — no skill or hand-written doc enumerates the
requiresFeaturevalues or the/auth/configfeature 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.
@objectstack/plugin-auth).getPublicConfig()addsfeatures.emailDelivery: this.isEmailDeliveryLive(). That is the one deliverability rule this package already has: the wired email service's ownisConfigured(), read per call, the rule sign-up and invite-member'soutcome: 'sent' | 'link_only'already use. It is nothasEmailTransport()/isEmailServiceAvailable(), which answer "a service object is wired" and are true on every default boot, whoselogtransport writes mail to the console. So: no email service,false; thelogtransport (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/configarm servesgetPublicConfig()unchanged, so it serves the flag too.@objectstack/spec).AuthFeaturesConfigSchemagainsemailDelivery: z.boolean().optional(), andPUBLIC_AUTH_FEATURESclassifies 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 arequiresFeaturevalue, lowered tofeatures.emailDelivery == true.outcomeon the reset answer. Measured both. The/auth/configflag has the exact precedent infeatures.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'sforgotPassword(email)resolves tovoidand 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.Tests
packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts, a new top-leveldescribein the reset door's existing harness (realAuthManager.handleRequestover a realObjectQLengine on better-sqlite3), 3 cases:features.emailDeliveryisfalse, and the reset request for an address with an account and one without answers the same200body;logshape):false, same answer for both addresses;true, both addresses answer the same, and that answer equals the one a deployment without mail gives.Updated: the exact
featuresfixture inauth-manager.test.ts(emailDelivery: false, no email service there), the served fixture inpackages/spec/src/api/auth-endpoints.test.ts, and the registry roster inpackages/spec/src/kernel/public-auth-features.test.ts(12 → 13 flags).Ablations (forbidden behaviour put back with `scripts/ablation-replace.mjs`, red seen, restored; the tests import `./auth-manager.js` from source)
emailDelivery: this.isEmailServiceAvailable()(the "service wired" reader):Tests 1 failed | 11 passed (12)— the log-transport case,AssertionError: expected true to be false. Restore: blob5bc1cba772b0…equals the HEAD blob.emailDeliveryline deleted:Tests 5 failed | 12 passed (17)over the reset harness andpublic-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.Acceptance notes
domain:ui), not in this PR.ui/packages/auth/src/ForgotPasswordForm.tsxand thesuccessDescriptionstrings inui/packages/i18nstill say the link was sent; the form should readfeatures.emailDeliveryfrom/auth/config(as it readsphoneNumberOtp) and, when it is nottrue, 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.400 EMAIL_SERVICE_REQUIREDwhen the transport is not live (visitor-verification-resend.ts), so it does not say "sent". Invite-member already answersoutcome: 'link_only'.POST /magic-link/sendstill answers success with no mail, but magic link is not advertised on/auth/configand no console UI calls it; a caller can now readfeatures.emailDeliveryfirst. Noted, not filed.Verification
Gates, suites and lint on head
f78457d8(origin/mainmerged in at that commit).Suites and typecheck
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 typecheckandpnpm --filter @objectstack/plugin-auth typecheck: exit 0.pnpm --filter @objectstack/spec check:generated:content/docs/references/**stale after the.describe()change;--fixregenerated exactly that one (theauth-endpointsandactionreference pages, the latter becauserequiresFeature's enum grew), committed.Derived gates: 106 derived, 105 exit 0, 1 NOT MEASURED
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 106 commands; each ran with its exit code captured before any pipe.--ranreconciliation: "106 derived famil(ies) accounted for — 105 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)".pnpm check:dual-build-cjs-loadsexits 3 (PREREQUISITE NOT MET, 35 packages withoutdist/; it needs a whole-workspace build). CI runs it.node scripts/check-plugin-teardown-shape.mjs --self-testexit 0 ("48 cases pass", aftergit fetch origin 621a4876…on the shallow checkout);check:skill-examplesexit 0 after building@objectstack/clientandclient-react("283 prose examples type-check").pnpm lint(eslint . --no-inline-config): exit 0.!and the Clause-② arm is(widening).Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed (authorable-surface/api.json, and the two auto-generated reference pages fromcheck:generated --fix).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/(the two reference pages are generator output).🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A