Repository navigation
fix(plugin-auth): a verification-email resend from a visitor who is not signed in answers the same whatever the send does - #30144
Conversation
…ot signed in answers the same whatever the send does Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…resolveActor in the before-hook Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…sign-up changeset's resend line to a signed-in caller Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check15 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 22 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 b09fc767ad4a6cd8af1022e8a1044cd703f876ce && git checkout b09fc767ad4a6cd8af1022e8a1044cd703f876ce
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55337f4ce35dbe62434b23517547a0b5052b936c 4200bbebc7fa86bc43e9e93422cbfaeb134c34d9 && git checkout -B drift-repro 55337f4ce35dbe62434b23517547a0b5052b936c && git merge --no-ff 4200bbebc7fa86bc43e9e93422cbfaeb134c34d9
node scripts/docs-audit/affected-docs.mjs --json 55337f4ce35dbe62434b23517547a0b5052b936c |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 4200bbe · 2026-11-04T16:32Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Inputs: the PR body, its file list, the net diff against the merge base Check-runs on the head: every check is completed; none is still running. All are green except ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Red by design · head Gate: Cause: this PR edits a pending changeset that it did not create: line 12 of Why the red does not block CI: Owed: the maintainer's written confirmation on this PR. The seat will not land the PR before it is posted. |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Changeset correction confirmed by the maintainer, 2026-11-05T00:47Z. The confirmation was given in session This is the written confirmation that |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
Clause-②: no
Metadata-protocol: none
维护者速读
POST /api/v1/auth/send-verification-email时,回答不再随发信结果变化。邮件通道可用时一律答{ "status": true },信在响应之后才发,发送失败只在服务端记一条 error 日志;邮件通道不可用时(未接邮件服务,或isConfigured()不为 true,例如log传输),访客的每次重发都以同一个400 EMAIL_SERVICE_REQUIRED拒绝。已登录用户(cookie 或 bearer)重发自己的地址、以及服务端auth.api调用,行为不变。log传输),访客点「重发」会看到「未配置邮件投递」的提示;邮件通道可用但发送失败时,访客看到「已发送」,失败只见于服务端日志与发信记录。已登录用户的重发不受影响。回滚即 revert 本 PR。packages/plugins/plugin-auth/src/visitor-verification-resend.ts(规则、访客判定与拒绝);auth-manager-auth-instance.ts的sendAfterResponseOnSignUpAndVisitorResend;auth-manager-before-hook.ts新增的一处调用。pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/signup-existing-address-success-shape.test.ts,预期 25 个用例全绿,其中 ⑧ 组 7 个。.changeset/sign-up-mail-after-response.md第 12 行的改写(把「重发仍等待并报告失败」限定为已登录用户的重发);Check Changeset因此按设计保持红,它不是必需检查。What changes
POST /api/v1/auth/send-verification-email) from a visitor who is not signed in answers the same whatever the send does:isEmailDeliveryLive()), it answers{ "status": true }. The mail is built and sent after the response throughsendAfterResponseOnPublicDoor, as the sign-up's verification mail already is, and a send that fails is logged on the server at error (verification email was not delivered);400 EMAIL_SERVICE_REQUIRED.auth.api.sendVerificationEmailcall, which carries noRequest, still awaits its send.ctx.context.session,nullfor a caller with none). In the before-hook, where that field cannot answer yet:AuthManager.resolveActor, the actor reader the other before-hook gates use, which reads the cookie and the bearer token alike.getSessionFromCtxalone reads a caller signed in by bearer token as no session there, because better-auth'sbearer()plugin applies its cookie only after every before-hook has run (measured; ablation leg 3 below pins it).visitor-verification-resend.ts(new, not exported from the package): the rule, the endpoint-context predicate, and the before-hook refusal.auth-manager-auth-instance.ts:sendAfterResponseOnSignUpbecomessendAfterResponseOnSignUpAndVisitorResend, which also hands the visitor's resend to the after-response path; comments in the callback scoped to the signed-in resend.auth-manager-before-hook.ts: one call, andisEmailDeliveryLiveon the hook's host.verification-email-failure-propagation.test.ts: header text scoped to the signed-in resend; assertions unchanged.visitor-verification-resend.md(@objectstack/plugin-authpatch).sign-up-mail-after-response.mdcorrected, see Acceptance notes.Tests
Pins in a new top-level describe ⑧ of
signup-existing-address-success-shape.test.ts, the real-engine harness the after-response sends already share.deferred()andgatedEmail()moved from ⑦ to module scope (gatedEmailgains athrowsoutcome) so ⑦ and ⑧ share them.200 {"status":true}while the send is still held open by a deferred, never by a wall-clock threshold. Released, the send settles and one error line is logged.logtransport'sisConfigured()answering false, a service withoutisConfigured(), no email service at all): the three addresses get the same400answer with codeEMAIL_SERVICE_REQUIRED, and nothing is sent.Request) still awaits its send and rejects with its failure.Results
One-shot ablation, three legs
Each leg went through
scripts/ablation-replace.mjs: anchor hit once, blob changed, the file run, then restored with blob == HEAD and an emptygit diff HEAD. The tests importsrc/relatively, so nodist/leg exists to prove.Acceptance notes
check-empty-changeset.mjsnames)..changeset/sign-up-mail-after-response.md(from fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account #29975, still unreleased onmain) listed "the/send-verification-emailresend, which still awaits its send and still reports one that failed" as unchanged. This PR keeps that true only for a signed-in caller, so the line now reads "a signed-in caller's/send-verification-emailresend, which still awaits its send and still reports one that failed". Restoring it would publish a sentence this PR makes false.Check Changesetstays red by design until the correction is confirmed here in writing; it is not a required context.EMAIL_SERVICE_REQUIREDis already in the ADR-0112 ledger for@objectstack/plugin-auth, and/change-emailand the identity import answer it for the same deployment condition. The constant is typed against the ledger, so a ledger change reds its compile. The body is better-auth's flat{ code, message }, the shape/change-email's coded refusals answer in; the message names the fix (Settings → Mail).Clause-②: no, measured. No spec schema, export or ledger entry changes. The new module is not re-exported: the builtdist/*.d.tsanddist/*.d.mtscarry none of its five names (control:AuthManager, 30 hits), andrequire('./dist/index.js')lists no new key.VerifyEmailPromptPageandRegisterPageshow the server'smessagewhen a resend fails, so a visitor sees the refusal's message while no transport is live; while one is live, a visitor's resend now always reads as sent. The docs' resend example (content/docs/permissions/authentication.mdx) is the signed-in, bearer-token call, which is unchanged.packages/qa/dogfood/test. No test sends or reads a/send-verification-emailmail. The only caller isupgrade-harness/cli-prerelease-fixture.ts, a fixture generator that is not a test and drives a pinned prerelease CLI, not HEAD code. Nothing to adapt.tests 7.44s(shared box).Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon head4200bbebc7derived 60 commands; all 60 were run with each exit captured before any pipe, then reconciled with--ran(60 derived, 59 run, 1 NOT-MEASURED, 0 UNRUN).node scripts/check-empty-changeset.mjs --base origin/mainexit 1: the deliberate-correction refusal above, red by design.pnpm check:dual-build-cjs-loadsexit 3, PREREQUISITE NOT MET (packages outside plugin-auth's closure have nodist/): NOT MEASURED locally; CI's build measures it.pnpm check:type-check-debtexit 0, re-run under the verify lock at4200bbebc7(re-measure OK: 0 raw tsc errors above any recorded number).node scripts/check-platform-checklist.mjs --changed-from origin/main: exit 0, no anchored file in the change set.