Skip to content

fix(plugin-auth): a sign-up answers without waiting for any mail send, whether or not the address already has an account - #29975

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/sec-hardening-n
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/sec-hardening-n

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:注册接口 POST /api/v1/auth/sign-up/email 不再等任何邮件发送完成就作答——新地址的验证邮件、以及发给已有账号地址的提醒邮件,都改在响应之后发出,沿用找回密码邮件已在用的同一个助手。
  • 为什么改:注册无论地址是否已有账号,都应一样作答、不等发信;这与找回密码请求已落地的规则一致。
  • 风险与回滚:响应后才发信,进程若在响应与发信之间退出会丢这封信(与原先请求中途退出等价);重发验证邮件接口仍同步等待并如实报错。回滚即 revert 本 PR。
  • 先看:auth-manager-auth-instance.ts 里的 sendAfterResponseOnSignUp 与两个回调的包装;signup-existing-address-success-shape.test.ts 的 ⑦ 组;run-after-response.ts 的日志一行。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/signup-existing-address-success-shape.test.ts,预期 18 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • The rule. POST /api/v1/auth/sign-up/email answers without waiting for any mail send, whether or not the address already has an account. On the public door (a call that carries its request) the verification mail and the notice to an address that already has an account are built and sent after the response, through sendAfterResponseOnPublicDoor, the helper the password-reset mail already uses. A send that fails is logged on the server at error and never reaches the caller, whose answer is the one a sent mail gets.
  • Only the sign-up's verification send moves. The verification-mail callback serves more doors than sign-up, so sendAfterResponseOnSignUp hands the helper only the call the sign-up endpoint makes, read by the endpoint's declared path from better-auth's endpoint context (as the hooks read ctx.path), never from the request URL. Every other call is awaited exactly as before, above all /send-verification-email, which still reports a failed send to its caller. Outside any endpoint context the call is awaited.
  • A server-side call is unchanged. auth.api.signUpEmail carries no request and still awaits both sends.
  • The failure log names its medium in what. logAfterResponseSendFailure now prints what as given (password-reset email, verification email, sign-up notice email). This hunk, and the reset call site's what, are byte-identical to the sibling draft that moves the phone reset text, so the two merge cleanly in either order (git merge-tree against its head: no conflict). The reset's own log line reads exactly as before. The notice keeps its own error line for a failed send and never throws; onFailure is its backstop.
  • Unchanged. The answer's status and body, the sign-up rate limits, the mails themselves.

Tests

  • signup-existing-address-success-shape.test.ts gains ⑦, one top-level describe in the existing harness file:
    • with sendOnSignUp off and on, both kinds of address get the same answer while every send is held open by a deferred, and the mail still goes out once it is released;
    • a verification send that fails is logged on the server at error, and the sign-up answers as one whose mail is sent;
    • the server-side sign-up call (no Request) still awaits its sends;
    • the /send-verification-email resend still awaits its send and answers 500 for one that failed.
  • The existing pins ①, ②, ⑤ and ⑥ wait for the mail to arrive (vi.waitFor) before reading it; what each asserts is unchanged. The negative mail assertions in ②, ③ and ④ follow an explicit refusal, where no send is ever scheduled.

Acceptance notes

  • A consumer that reads the verification mail the instant the sign-up answers now sees it a moment later; the plugin-auth suite has no such reader left, and a git grep over test files outside this package finds none.
  • Ablation (one-shot, each leg restored and proven by blob hash): removing the notice wrapper reds both ⑦ answer cases; removing the verification wrapper reds the sendOnSignUp on answer case and the failure-log case; wrapping every verification call reds the resend case. Removing a wrapper fails by the case timeout, because the answer then waits on the held send.

Verification

plugin-auth suite at 7a467a8
pnpm --filter @objectstack/plugin-auth test   (under scripts/pm/os-verify-lock.sh)
 Test Files  219 passed (219)
      Tests  3771 passed | 10 skipped (3781)
os-verify-lock: VERDICT command-exit 0
plugin-auth typecheck at 7a467a8
pnpm --filter @objectstack/plugin-auth typecheck   (under scripts/pm/os-verify-lock.sh)
check:test-typecheck: OK — 10 file(s) / 88 error(s) / 23 pinned signature(s) held in test-typecheck-debt.json
os-verify-lock: VERDICT command-exit 0
Ablation at 6a399c2 (source identical at 7a467a8)
leg notice   (onExistingUserSignUp wrapper removed)       exit 1 · 2 failed | 3 passed (⑦ only) · restored: blob == HEAD
leg verify   (sendVerificationEmail wrapper removed)       exit 1 · 2 failed | 3 passed (⑦ only) · restored: blob == HEAD
leg wrap-all (every verification call after the response)  exit 1 · 1 failed | 4 passed (⑦ only) · restored: blob == HEAD
Derived gates at 7a467a8
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack   # 59 commands, tree 7a467a8031
# every command run, exit code captured before any pipe; reconciled with --ran:
#   59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

pnpm check:dual-build-cjs-loads :: exit 3   # PREREQUISITE NOT MET: reads every package dist/, this tree built plugin-auth and its closure only
#   NOT MEASURED locally; Lint & Repo Gates runs it. Targeted substitute: require() of plugin-auth dist/index.js loads (160 exports).

# also run, rosters under the changed paths (outside the derivation):
pnpm check:auth-mount-ledger :: exit 0
pnpm check:authz-resolver :: exit 0
pnpm check:changeset-fixed :: exit 0
pnpm check:error-code-casing :: exit 0
pnpm check:filter-alias-parity :: exit 0

CI note (seat)

  • Type Check · workspace on 7a467a8031 failed because Checkout repository timed out after 20 minutes, before any step ran on the code (the same runner-side timeout hit merge-group run 41719580038 the same hour). Treated as a flake and re-run once through the relay.
  • Check Changeset on this head was cancelled with no superseding run; this body edit re-triggers it.

…, whether or not the address already has an account

On the public door the sign-up's mail - the verification mail, or the
notice to an address that already has an account - is built and sent
after the response, through the same helper the reset mail uses; a send
that fails is logged on the server at error. A server-side call that
carries no request, and every other caller of the verification mail
(the resend among them), still awaits its send.

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
…d, whether or not the address already has an account

Pins, with sendOnSignUp off and on, that both kinds of address get the
same answer while every send is still open and that the mail still goes
out after the response; that a failed verification send is logged on the
server; that the server-side sign-up call still awaits its sends; and
that the resend still awaits its send and reports one that failed. The
existing pins read the mail once it has arrived. Adds the changeset.

Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 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
  • 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 — 22 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 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 732ea91dde26eac6c93abbc0db57fbb2c5f401ac — the merge of head a58c8e55b316c3293a1ab15bbd292f4bce6f9bc2 into base 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77, 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 732ea91dde26eac6c93abbc0db57fbb2c5f401ac && git checkout 732ea91dde26eac6c93abbc0db57fbb2c5f401ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 a58c8e55b316c3293a1ab15bbd292f4bce6f9bc2 && git checkout -B drift-repro 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 && git merge --no-ff a58c8e55b316c3293a1ab15bbd292f4bce6f9bc2

node scripts/docs-audit/affected-docs.mjs --json 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77

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

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj

席位核对

ACCEPT · head a58c8e55b316c3293a1ab15bbd292f4bce6f9bc2 · 2026-11-03T22:46Z

  • Shape: 4 files, +231/−18. Not governed. Under 3000. Patch for plugin-auth. Clause-②: no, Metadata-protocol: none.
  • Review: this PR touches no contract face, so the seat read it itself at TIER_DEFAULT; no second agent.
  • Rule held (the maintainer's ruling that a mail send leaves the request path, alongside ruling 1B on cloud 测试发现(框架部分):注册接口的邮箱存在性、邮箱验证只能全开或全关、展示型字段原样存储 HTML(细节按披露裁定隐去) #25040): POST /sign-up/email answers without waiting for any mail send, whether or not the address already has an account.
    • Both the notice and the sign-up's verification mail go through the existing sendAfterResponseOnPublicDoor helper.
    • The verification mail is scoped to the sign-up endpoint by the vendor's declared endpoint path (sendAfterResponseOnSignUp). This is a predicate over the same helper, not a new send mechanism, and with no endpoint context it falls back to awaiting.
    • The /send-verification-email resend and server-side calls still await their sends.
  • Dev's open questions, answered: (1) keep the endpoint-context path (A). (3) keep the mirror of fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does #29973's log hunk (A); fix(plugin-auth): the phone-number password-reset request answers without waiting for the SMS send, as the emailed reset does #29973 has merged since and the branch is up to date. (2) The residual is on the maintainer's decision list. It does not block this landing.
  • Changeset prose, checked against the diff: "answers without waiting for any mail send" holds for both callbacks wrapped. "A send that fails is logged on the server" holds through logAfterResponseSendFailure with the medium in what. "Unchanged … the resend … still reports one that failed" holds: the predicate excludes it, and pin ⑦(d) covers it. The prose is rule-level only (§6).
  • Check-runs at this head, latest per name: 35 success, 13 skipped, 0 failed, all completed. Skips were judged by name: opt-in jobs and no UI change. check-expected-skips.mjs was NOT MEASURED. mergeable_state was read as clean before arming.
  • CI note: the previous head's Type Check · workspace failed because the checkout timed out, which the relay cannot re-run (lint.yml). The head moved by a relay pr_update_branch once main moved, and the fresh run is green. The flake reason is recorded in the PR body.
  • Local gates (dev): dispatch-gates --ran reported 59 derived, 58 run, and 1 NOT MEASURED (check:dual-build-cjs-loads), which is green in CI's Lint & Repo Gates at this head. The full plugin-auth suite has 3771 passed. The three-leg ablation went red, then was restored green.
  • Landing: ready, then auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 22:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit a7293f7 Nov 3, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/sec-hardening-n branch November 3, 2026 23:11
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant