Skip to content

fix(auth): say on /auth/config whether email leaves the deployment, so forgot-password can stop claiming a link was sent - #30365

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xry5up
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xry5up

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

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).

Ablations (forbidden behaviour put back with `scripts/ablation-replace.mjs`, red seen, restored; the tests import `./auth-manager.js` from source)
  • 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.

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).

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 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.
Derived gates: 106 derived, 105 exit 0, 1 NOT MEASURED
  • 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 621a4876… 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).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 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).
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/ (the two reference pages are generator output).
  • 8. Title, body and comments are in English (the 维护者速读 block is Chinese by the Communication rule).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CbSX6BdpVNjS79LB5A

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>
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>
@github-actions github-actions Bot added the size/m label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx (via getPublicConfig (symbol, a method of class AuthManager))
  • content/docs/protocol/objectui/actions.mdx (via PUBLIC_AUTH_FEATURES (symbol, a top-level const object))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PUBLIC_AUTH_FEATURES (symbol, a top-level const object))
  • content/docs/releases/v18/18-2.mdx (via AuthManager (symbol, a top-level class), PUBLIC_AUTH_FEATURES (symbol, a top-level const object))
  • content/docs/releases/v18/18-3.mdx (via AuthManager (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eeddf619b4a05f43c1f921ee118bfc08a5890ae4 — the merge of head a211f203b852238b9b79a100125139e877f0f142 into base fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f78457d8c2778d23a2760b0ac187b63b2d8da96b
Local-runs: none
Scope: packages/spec/src/api/auth-endpoints.zod.ts, packages/spec/src/kernel/public-auth-features.ts, packages/spec/src/api/auth-endpoints.test.ts, packages/spec/src/kernel/public-auth-features.test.ts, packages/spec/authorable-surface/api.json, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-manager.test.ts, packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts, content/docs/references/api/auth-endpoints.mdx, content/docs/references/ui/action.mdx, .changeset/30107-reset-without-email.md + packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts (sendResetPassword, the reset door's answer), packages/plugins/plugin-auth/src/auth-plugin.ts (the GET /config arm), packages/plugins/plugin-auth/src/public-feature-registry.test.ts (the drift guard), packages/plugins/plugin-auth/src/invitation-delivery-outcome.ts, packages/spec/src/contracts/email-service.ts (IEmailService.isConfigured), packages/plugins/plugin-email/src/email-service.ts (isConfigured, setTransport), packages/spec/src/ui/action.zod.ts (requiresFeature lowering), packages/spec/scripts/build-schemas.ts and packages/spec/scripts/build-docs.ts (the generators), packages/spec/json-schema.manifest/api.json, scripts/check-changeset-no-major.mjs (the level axis), AGENTS.md Post-Task Checklist step 3, .claude/skills/pm-dispatch/SKILL.md claim template and references/review-checklist.md, .claude/agents/os-dev.md Definition of done

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

  • AuthFeaturesConfigSchema.emailDelivery: z.boolean().optional() (auth-endpoints.zod.ts:265): a new optional member on the published GET /api/v1/auth/config response. The schema is a plain z.object (strip, not strict), so an older client parsing a newer server's payload drops the key rather than refusing it. Widening, named right. The describe text matches the served semantics read in code (below) and the generated row byte for byte.
  • PUBLIC_AUTH_FEATURES.emailDelivery = { surface: 'login', semantics: 'opt-in', exempt } (public-auth-features.ts:286): PUBLIC_AUTH_FEATURE_NAMES is Object.keys(PUBLIC_AUTH_FEATURES) and feeds z.enum on ActionSchema.requiresFeature and ActionParamSchema.requiresFeature (action.zod.ts:498, :1688), so requiresFeature: 'emailDelivery' is a new accepted authorable value. lowerRequiresFeature lowers it through FEATURE_GATE_OPERATORS['opt-in'] to features.emailDelivery == true — correct for a flag whose default is false (no mail), and consistent with the drift guard case "registry default semantics match the served defaults", which at defaults (no email service) reads false. login plus exempt is the shape every other login-surface flag carries (sso, ssoEnforced, deviceAuthorization, phoneNumberOtp). Named right.
  • The served value, getPublicConfig().features.emailDelivery = this.isEmailDeliveryLive() (auth-manager.ts:4657): the one reader of IEmailService.isConfigured(); false when no service, when the member is absent, when it throws, or when it answers false; true only on === true. Read per call from this.config.emailService, never cached. That is the rule invite-member's outcome already uses (invitation-delivery-outcome.ts) and not isEmailServiceAvailable() / hasEmailTransport(), which are !!this.getEmailService() and true on every default boot. The PR's claim about the three helpers holds against the code.
  • GET /config arm (auth-plugin.ts:3159): getPublicConfig() is served whole, only features.sso is refined in place, so the flag reaches the wire unchanged. No second projection of the features shape exists that would now disagree (discovery.zod.ts reads only features.admin; system/auth-config.zod.ts carries none).
  • Generated artifacts: authorable-surface/api.json gains exactly api/AuthFeaturesConfig:emailDelivery in the shard's sorted position (between deviceAuthorization and the retired magicLink), the shape build-schemas.ts writes; references/api/auth-endpoints.mdx gains one row per occurrence of the schema (two tables) whose text equals the .describe() concatenation; references/ui/action.mdx moves … +2 more to … +3 more on both requiresFeature rows and appends emailDelivery last in both Allowed Values lists, which is the registry's key insertion order. json-schema.manifest/api.json is a name ledger and already lists api/AuthFeaturesConfig, so it correctly did not move; no public export was added, so api-surface/ correctly did not move. All three pages read as generator output, not hand edits.

② Semver level

  • Clause-②: yes (widening): a new optional response member and a widened enum; the arm is right, no narrowing anywhere in the diff.
  • Metadata-protocol: added: AGENTS.md step 3 — "a new key, accepted value or metadata type is added". requiresFeature: 'emailDelivery' is a new accepted value, so added is right and the claim's none was wrong on measurement; the dev's re-declaration is the correct act, and the level axis (check-changeset-no-major.mjs, verdict none-baseline-moved) would have refused none over the moved authorable-surface/api.json shard. Check Changeset is green on this head.
  • Levels: @objectstack/spec: minor, @objectstack/plugin-auth: patch. Step 3: "a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch". The protocol change (the accepted value) lives in spec, which is minor; plugin-auth's hunk is a served flag, non-protocol, patch. The gate reads the same way (its refusal fires only when "no moved package is graded minor or above"). Right.
  • Declaration lines identical in value in PR body and changeset: yes (widening) / added in both; the PR body's lines carry trailing prose after an em dash, the value is first after the colon in each. Fine.
  • Skills-docs: line: AGENTS.md step 3 names only the two declaration lines. The third line is owed by the claim template (.claude/skills/pm-dispatch/SKILL.md: "a kind other than none, or a (narrowing) arm, owes the PR body a third line the dev writes from the diff: Skills-docs: updated here | follow-up owed — FILES | follow-up #N | none — REASON") and by .claude/agents/os-dev.md Definition of done ("种类非 none ⇒ 按 diff 加写 Skills-docs: 行"); references/review-checklist.md grades its absence: "Skills-docs: 行在且属实 … 缺 ⇒ REWORK". The kind here is added. The PR body carries no Skills-docs: line. must-fix (M1) — see ④.

③ Boundary flags

  • Metadata-protocol re-declared none → added by the dev: accepted, it is the measured value (② above). The claim allowed re-declaration of Clause-② only; re-declaring the sibling line to the true kind is the honest act and the gate would have forced it anyway.
  • Files outside the claimed surface: packages/spec/src/kernel/public-auth-features.ts + test — forced by public-feature-registry.test.ts ("every boolean flag served by getPublicConfig() is classified in the registry, and vice versa"), so the registry entry is not optional once the flag is served; packages/spec/authorable-surface/api.json, content/docs/references/api/auth-endpoints.mdx, content/docs/references/ui/action.mdx — verified generator output (①), and content/docs/references/ is AUTO-GEN by the Documentation Guardrails, which is exactly the regenerate-and-commit the §Touched packages/spec rule requires. The order's ⛔ content/docs/** reads as a ban on hand edits; generator output is not that. Accepted as reported deviations.
  • Carrier (a) /auth/config flag over (b) a member on the reset answer: the stated grounds hold in code — features.phoneNumberOtp is the same form's existing precedent (ui/packages/auth/src/ForgotPasswordForm.tsx:157 reads it before submit), and a reset-answer member would be invisible to a page that must speak before the submit. The reset answer is untouched. Accepted.
  • Merge commit f78457d8 with git's default message: reported; not a contract matter.
  • Check-runs on this head, last read 2026-11-05T08:15Z (polled four times from 08:07Z, two-minute spacing): 62 runs; 23 success, 8 skipped, 1 failure (Spec property liveness, below), 30 still in progress — among them Build Core, Type Check UI, Build & E2E UI, Live E2E UI, every Test Core and Test UI shard and the three Type Check · lanes. Green so far: Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, PR-body guards, Check PR Size. The ten-minute wait budget ran out before the required set completed; this record judges the tree and the declarations with what is there and says so — a verdict on the gates themselves is the seat's at landing, once they conclude.
  • Pre-existing red: Spec property liveness fails on this head with query/searchFields → packages/objectql/src/engine.ts (an unanchored ledger citation in objectql), and the same job fails identically on origin/main at fdad1786, this PR's base. Not introduced here; the check is not one of the ten required contexts. The seat's landing rule on a main-red still applies to the lane, not to this record.

④ Design judgment

  • Account-existence oracle: the reset door answers { status: true, message } for any address; a known address additionally schedules sendResetPassword after the response (sendAfterResponseOnPublicDoor, auth-manager-auth-instance.ts), whose throw on no service reaches only the log. The new pins compare { status, body text } for a known and an unknown address with toEqual in all three transport states (no service; a service whose isConfigured answers false; one whose isConfigured answers true), and the third case also pins the live answer equal to the no-mail answer. Byte-identical, timing aside (the send is deferred past the answer on the public door, so the known address does not even pay the send before answering). Holds.
  • Leak: features.emailDelivery is a deployment property, equal for every caller, on a payload that already tells an anonymous caller phoneNumberOtp (SMS deliverability), sso, ssoEnforced, audiencePosture and the Turnstile site key. "This deployment sends no mail" enables no attack on an account and is already visible to any member through invite-member's link_only. Same class as the phoneNumberOtp precedent. No finding.
  • Per-request cost: IEmailService.isConfigured is declared synchronous and "read at call time"; plugin-email's implementation is !(this.options.transport instanceof LogTransport) — no I/O, no await. isEmailDeliveryLive wraps it in try/catch. Negligible on the hot public endpoint. No finding.
  • Runtime flip: plugin-email's setTransport() hot-swaps options.transport, and the manager reads config.emailService per call, so a transport configured from the mail settings flips the flag on the next /auth/config read without a restart. A /auth/config answered before kernel:ready injects the service reads false for that one window; transient, and the safe direction. No finding.
  • log transport reads false in every environment: by the IEmailService contract ("A log transport … is NEVER delivering, in any environment") and the same as invite-member. Note for the domain:ui card: isPhoneOtpDeliverable treats a log SMS transport as deliverable outside production, so the forgot-password page will, in local dev with the stock mail transport, see emailDelivery: false while the link is printed to the console — the page should say a link cannot be emailed here and still let the submit through, never block it. Not a defect in this PR.
  • M1 (must-fix): the PR body owes a Skills-docs: line for a non-none kind and has none. No skill or hand-written doc hand-enumerates the requiresFeature values (skills/objectstack-platform/references/plugin-development.md and content/docs/protocol/objectui/actions.mdx point at the registry by name; content/docs/permissions/authentication.mdx names only phoneNumberOtp), and the generated references pages already carry the new value, so an honest line is Skills-docs: none — no skill or hand-written doc enumerates the requiresFeature values; the generated reference pages carry the new one or Skills-docs: updated here on the same grounds. The tree is otherwise sound: this is a PR-body edit, no new commit is needed, and a re-review on this same head follows the edit.
  • No should-fix.

Implemented-by: session_01Utp6CbSX6BdpVNjS79LB5A
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: FAIL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f78457d8c2778d23a2760b0ac187b63b2d8da96b
Local-runs: none
Scope: packages/spec/src/api/auth-endpoints.zod.ts, packages/spec/src/kernel/public-auth-features.ts, packages/spec/src/api/auth-endpoints.test.ts, packages/spec/src/kernel/public-auth-features.test.ts, packages/spec/authorable-surface/api.json, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-manager.test.ts, packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts, content/docs/references/api/auth-endpoints.mdx, content/docs/references/ui/action.mdx, .changeset/30107-reset-without-email.md + packages/plugins/plugin-auth/src/auth-manager-auth-instance.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/public-feature-registry.test.ts, packages/plugins/plugin-auth/src/invitation-delivery-outcome.ts, packages/spec/src/contracts/email-service.ts, packages/plugins/plugin-email/src/email-service.ts, packages/spec/src/ui/action.zod.ts, packages/spec/scripts/build-schemas.ts, packages/spec/scripts/build-docs.ts, packages/spec/json-schema.manifest/api.json, scripts/check-changeset-no-major.mjs, AGENTS.md Post-Task Checklist step 3, .claude/skills/pm-dispatch/SKILL.md claim template and references/review-checklist.md, .claude/agents/os-dev.md Definition of done

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 f78457d8, nothing was pushed. Inputs re-read: the PR body as of its 08:18:22Z update, and the head's check-runs in one poll. Every reading of 6653160903 that is about the tree, the generators, the declarations' values and the design stands as written there and is not restated in full; the lines below carry what changed and the verdict.

① Derived judgments

Unchanged from 6653160903: AuthFeaturesConfigSchema.emailDelivery (optional boolean, strip-not-strict object) is a widening; PUBLIC_AUTH_FEATURES.emailDelivery (login, opt-in, exempt) adds requiresFeature: 'emailDelivery', lowered to features.emailDelivery == true; the served value is isEmailDeliveryLive() read per call, not isEmailServiceAvailable(); the GET /config arm serves the object whole; authorable-surface/api.json and both references pages read as generator output; json-schema.manifest and api-surface correctly unmoved. Standing.

② Semver level

  • Clause-②: yes (widening) and Metadata-protocol: added: standing, values identical in PR body and changeset; spec minor, plugin-auth patch as before.
  • M1 closed: PR body line 10 now reads 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. The value none is first after the colon, the form the claim template allows (none — REASON), and the reason is true against the diff: skills/objectstack-platform/references/plugin-development.md and content/docs/protocol/objectui/actions.mdx point at the registry by name without listing its values, content/docs/permissions/authentication.mdx names only phoneNumberOtp, and the two generated pages in this PR carry the new row and the new allowed value. 「行在且属实」 holds.

③ Boundary flags

  • Standing from 6653160903: the re-declared Metadata-protocol, the forced out-of-surface files (drift guard; generator output), carrier (a) over (b), the default-message merge commit.
  • Check-runs on this head, read 2026-11-05T08:19Z: 66 runs (four more than at 08:15Z — the body edit re-triggered the PR-body workflows); 31 success, 10 skipped, 24 in progress, 1 failure. Newly green since the earlier record: Build Core, Build & E2E UI, Live E2E UI. Still running: Type Check UI, Type Check · workspace, five Test Core shards, all sixteen Test UI shards, and a fresh Check Changeset run on the edited body (its earlier run on this head was green). Nothing new is red. The required set has not concluded; this record judges the tree, the declarations and the design, and the gates' own verdict is the seat's at landing once they finish.
  • Pre-existing red, unchanged: Spec property liveness (completed 08:06:05Z) fails on query/searchFields → packages/objectql/src/engine.ts, exactly as it fails on origin/main at fdad1786, this PR's base. Not introduced here; not one of the ten required contexts.

④ Design judgment

Unchanged 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); features.emailDelivery is deployment-level and of the same class as phoneNumberOtp; isConfigured() is a synchronous instanceof check with no I/O; a transport configured at runtime flips the flag on the next read; the log transport reads false by the IEmailService contract (the domain:ui note stands: the page must say a link cannot be emailed here and still let the submit through). No must-fix. No should-fix.

Implemented-by: session_01Utp6CbSX6BdpVNjS79LB5A
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

  • 改了什么: GET /api/v1/auth/config 的 features 新增 emailDelivery(部署是否真的能发邮件,按调用时实时判定),登录页的「忘记密码」等依赖邮件的动作可以按它显示或隐藏(requiresFeature: 'emailDelivery')。
  • 为什么改: 没配邮件服务的部署上,「忘记密码」照样提示「已发送」,用户永远收不到(Forgot password says the reset link was sent on a deployment with no email service #30107)。界面半边是 Forgot password page: read features.emailDelivery and say a reset link cannot be emailed here (ask an administrator) instead of saying it was sent #30366。
  • 风险与回滚: 只是加宽:新增一个可选的配置字段和一个可选的功能名,旧客户端不读即无影响。回滚即撤本 PR。
  • 先看: packages/plugins/plugin-auth/src/auth-manager.ts 的 getPublicConfig,再看 packages/spec/src/kernel/public-auth-features.ts 新条目。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/kernel/public-auth-features.test.ts src/api/auth-endpoints.test.ts。
  • 需要您做的: 无,席位经合并队列落地(非受管面,107 行)。
  • 席位意见: ACCEPT。契约复审 FAIL 6653160903(只缺 Skills-docs: 行,席位已补正文)→ PASS 6653217513;合入 main 后增量逐行核对不变。

席位核对 — ACCEPT · domain:server#2 · head a211f203b8

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 10:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 10:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 09720e2 Nov 5, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xry5up branch November 5, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Forgot password says the reset link was sent on a deployment with no email service

1 participant