Repository navigation
Description
domain:server#1 · os-bob · session_012dn2AdKZirKLfeBMp7zEGS
Filing-gate category: ① — a user-visible product defect, reproduced on a public entry (production Console, POST /api/v1/auth/request-password-reset).
Maintainer direct order (routing and priority set from it, quoted verbatim):
发送重置链接 按钮 如果快速多次点击会出现 这个错误. 按规则给平台报issue, 标记为优先.
What happens
On the hosted Console's forgot-password page (cloud.objectos.ai, ObjectOS Cloud, zh-CN UI), entering an email and clicking 发送重置链接 / Send reset link several times quickly makes the form show a red banner:
Auth request failed with status 500
DevTools (maintainer's screenshot) shows one forgot-password document request followed by three request-password-reset XHRs from the burst; at least one of them answered 500. The banner text is the client fallback in ui/packages/auth/src/createAuthClient.ts:494-496 — i.e. the 500 body carried no message, so this is an unhandled server error, not a deliberate refusal.
Expected
- A burst of reset requests for one address never answers 5xx. Each request answers the uniform success body (no account-existence oracle), or — once the configured
rateLimit.customRules['/request-password-reset']budget is spent — a 429 with its message, which the form already displays. - The page does not fire a second request while one is in flight.
Reach
Measured once on the public door: production Console, Nov 2026, cloud's framework pin c5758b0dc36a (.objectstack-sha). Repro: open /forgot-password, type any address, click the submit button 3–5 times as fast as possible → banner above, a 500 in the network tab.
Where to look (not yet pinned — first step is a local repro)
The 500 is server-side and concurrency-shaped; the single request path is covered by reset-request-mail-after-response.test.ts, which never sends two at once. Candidates, in reading order:
- Rate-limit counter store under a same-key burst —
packages/plugins/plugin-auth/src/rate-limit-storage.ts(createLazyCacheRateLimitStorage, the cache-backedcustomStoragewired inauth-plugin.ts:788-792) and thecustomRulesbind atauth-plugin.ts:2635-2653.rate-limit-concurrency.test.tspins the admitted count for a burst, not that no request in the burst throws; a write that conflicts on the shared cache (e.g. a duplicate-key insert on first touch of a window) would surface as an unhandled 500. - The reset mint itself — better-auth's
requestPasswordResetverification-row write and thesendResetPasswordcallback inauth-manager-auth-instance.ts:~490-560(readAccountInvitation,storedRecipientLocale,runAfterResponse) racing for the same user. - Client side (secondary,
domain:ui) —ui/packages/auth/src/ForgotPasswordForm.tsx:227-241handleSubmithas no in-flight guard; the button'sdisabled={isLoading}(:465) only takes effect after a re-render, so a fast double/triple click submits 2–3 times. Also the fallback banner is untranslated English in a zh-CN session.
The server fix is the defect; the client guard only hides it (a script or a slow network still bursts), so it rides along but does not close this card on its own.
Acceptance
- A plugin-auth test drives N (≥5) concurrent
POST /request-password-resetfor one address throughAuthManager.handleRequestwith the cache-backed rate-limit storage and asserts every answer is 200 or 429 — none 5xx — and that the server logs no unhandled error. ForgotPasswordFormignores a submit while one is pending (test: two synchronous submits → oneforgotPasswordcall).- Changeset for
@objectstack/plugin-auth(and theui/auth package if touched).
Business reason
Who benefits: every end user and admin who forgets a password on a hosted or self-hosted deployment — this is the only self-service recovery path. Scenario: an impatient user double-clicks (or the mail is slow and they click again). Cost of not fixing: a red "status 500" on a sign-in surface reads as "the service is broken", the user cannot tell whether a mail was sent, and every burst is a server error in the logs.
Reader
The domain:server seat (plugin-auth owner) takes the server repro and fix; the client guard in step 3 is a one-line rider in the same PR or a domain:ui follow-up.
Duplicate search
repo:objectstack-ai/objectstack is:issue (open and closed), 2026-11-12: request-password-reset 500 (13 hits), password reset 500 (73), forgot password 500 (6), request-password-reset concurrent (16), password reset double submit (40), reset rate limit 500 (35). None describes a 5xx from a reset-request burst; nearest are #21793 (phone send-otp 500 with no SMS provider, closed, different route/cause) and #32021 (self-hosted first-run UX, open, no 500).
Activity
objectstack-fleet commentedon Nov 12, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: first touch (requested by
domain:server#4) →domain:server· p1 ·pm:queue· 2026-11-12T15:51Z⛔ Not a claim. This adds the triage audit the card lacked.
domain:server(packages/plugins/plugin-auth, therequest-password-resetpath under a burst). The second expectation, no second request while one is in flight, isui/packages/authand may be split todomain:ui.Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX
Claim: PM loop round 2
Session:
session_01L6bhgWRcTEB3toGxnWvuvXAccount:
os-salesBranch:
claude/issue-32645-reset-burst-no-5xx(the card thread's harness branch replaces it by amendment)Worktree:
objectstack-issue-32645Domain:
domain:serverSeat:
domain:server#4File surface:
packages/plugins/plugin-auth/src/**(therequest-password-resetpath:rate-limit-storage.ts, thecustomRulesbind inauth-plugin.ts, the reset mint /sendResetPasswordinauth-manager-auth-instance.ts, and their tests); riderui/packages/auth/src/ForgotPasswordForm.tsx(in-flight submit guard) and its test;.changeset/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable),effort: xhigh(p1, card thread)Clause-②: no
Metadata-protocol: none
Responsibility:
objectstack: plugin-auth answers an unhandled 500 to a same-address burst on POST /request-password-reset | the platform path that already covers it: the configured rateLimit customRules budget answering 429, and the uniform success body | every user on the self-service recovery door; reached today on productionThread-read: 6828471652
Premise: holds (the card's local-repro step is the first thing the dev measures; the single-request path is pinned, the burst path is not)
Serial constraints cleared: open PR #32669 (SSO account creation) touches
auth-manager-auth-instance.ts, a different region (SSO account provisioning vs. reset mint); PR #32194 (parkedpm:on-hold, #31948) touchesauth-plugin.tsorigin binding, a different region. Parallel by region; merge main before PR and after either lands.Order: the maintainer's direct order quoted on the card (「标记为优先」) puts it first in this seat's pick order; taken as a third running slot within the server seats' ceiling of 5.
Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX
Claim amendment to 6829682740 · 2026-11-12T16:39Z
Branch:
claude/project-thread-6kjsreCard thread:
session_01L6bhgXjcwwHcA9BB8HRHe9(mode:thread; the thread's harness can push only this branch). Every other line of 6829682740 stands.Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX
Claim: PM loop round 2
Session:
session_01L6bhgWRcTEB3toGxnWvuvXAccount:
os-salesBranch:
claude/project-thread-6kjsreWorktree:
objectstack-issue-32645Domain:
domain:serverSeat:
domain:server#4File surface:
packages/plugins/plugin-auth/src/**(therequest-password-resetpath:rate-limit-storage.ts, thecustomRulesbind inauth-plugin.ts, the reset mint /sendResetPasswordinauth-manager-auth-instance.ts, and their tests); riderui/packages/auth/src/ForgotPasswordForm.tsx(in-flight submit guard) and its test;.changeset/**(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable),effort: xhigh(p1, card thread)Clause-②: no
Metadata-protocol: none
Responsibility:
objectstack: plugin-auth answers an unhandled 500 to a same-address burst on POST /request-password-reset | the platform path that already covers it: the configured rateLimit customRules budget answering 429, and the uniform success body | every user on the self-service recovery door; reached today on productionThread-read: 6828471652
Premise: holds (the card's local-repro step is the first thing the dev measures; the single-request path is pinned, the burst path is not)
Serial constraints cleared: open PR #32669 (SSO account creation) touches
auth-manager-auth-instance.ts, a different region (SSO account provisioning vs. reset mint); PR #32194 (parkedpm:on-hold, #31948) touchesauth-plugin.tsorigin binding, a different region. Parallel by region; merge main before PR and after either lands.Order: the maintainer's direct order quoted on the card (「标记为优先」) puts it first in this seat's pick order; taken as a third running slot within the server seats' ceiling of 5.
Restates: 6829682740 with amendments 6829734804 folded in, so the claim's own
Branch:line names the card thread's branch (same session, same seat; not a second claim).Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-sales · session_01L6bhgXjcwwHcA9BB8HRHe9
os-dev-report
{ "issue": 32645, "status": "done", "branch": "claude/project-thread-6kjsre", "pr": "https://github.com/objectstack-ai/objectstack/pull/32788", "pr_closing": "Part of #32645 (the card stays open for the production 500)", "head": "03979ba637d8c21c7f7232aff837b675478dcb88", "session": "session_01L6bhgXjcwwHcA9BB8HRHe9", "claim": "6830595344", "premise_still_valid": true, "premise_note": "Client half measured and fixed. Server half not reproduced in the framework's own composition (runs listed in the PR body); not falsified either, since the card's 500 is a production reading.", "summary": "ForgotPasswordForm now ignores a submit while its reset request is pending (a ref read and set synchronously in the handler) and disables the button and email field until the request answers; a failed request, 429 included, releases the form. Its only guard before was the provider isLoading, which tracks the session read and is never raised for forgotPassword. The plugin-auth acceptance pin runs 8 concurrent POST /request-password-reset for one address through AuthManager.handleRequest with createLazyCacheRateLimitStorage over MemoryCacheAdapter and the customRules entry the settings bind writes (3 per 60 s): 3x200 plus 5x429, one uniform admitted body, no error logged, same multiset for an address with no account.", "root_cause": { "client": "measured. Ablation of the guard: AssertionError: expected \"vi.fn()\" to be called 1 times, but got 2 times (two synchronous submits, two forgotPassword calls).", "server": "not reproduced. AuthManager level 8 concurrent, CRM os start NODE_ENV=production on better-sqlite3 (6) and on Postgres 16 (6 and 20, live and revoked cookie): every answer 200 or 429, zero server error lines. Only two paths answer a 500 with no top-level message: an exception inside a vendor endpoint (router answers an empty-bodied 500) or an exception out of handleRequest (host catch). Sensitivity ablation: a counter store that throws on the second write to one key rejects out of handleRequest, which a host answers as exactly that shape. The deciding reading is the production log line at that moment (one of Auth request error:, # SERVER_ERROR:, better-auth returned error) and the cache service the hosted runtime registers." }, "tests": "pnpm --dir ui exec vitest run packages/auth/: Test Files 45 passed (45), Tests 461 passed (461). plugin-auth reset-request-mail-after-response.test.ts: Tests 14 passed (14). plugin-auth whole package on 3c28e4b5 (pre-merge of main): Test Files 265 passed (265), Tests 4943 passed, 10 skipped. Post-merge on 03979ba6: reset harness plus rate-limit-concurrency and rate-limit-storage tests 43 passed (43), ui auth 461 passed. Ablations: client guard removed, pin red as above, restored blob 945d49f7a67c equal to HEAD; server pin with a throwing counter store, Tests 2 failed, restored blob 8bcd840c61fc equal to HEAD, empty git diff HEAD.", "gates": "pnpm lint exit 0. @object-ui/auth type-check exit 0. @objectstack/plugin-auth typecheck exit 0 (check:test-typecheck OK). dispatch-gates --commands derived 66, each run with its exit recorded: 66 exit 0. --ran: 66 derived famil(ies) accounted for, 66 run, 0 NOT-MEASURED, 0 UNRUN. check-closing-keyword-parity --body OK. PR CI at report time: 35 check-runs, 6 success, 10 skipped, 19 in progress, 0 failed; the seat reads the finished set at ACCEPT.", "line_budget": "no cap named in the brief; diff 203 insertions, 6 deletions over 4 files; production code 18 insertions, 5 deletions in one file (ForgotPasswordForm.tsx); the rest is tests and one changeset.", "files_changed": [ ".changeset/forgot-password-one-request-in-flight.md (+10)", "ui/packages/auth/src/ForgotPasswordForm.tsx (+18 -5)", "ui/packages/auth/src/__tests__/forgot-password-success-message.test.tsx (+69)", "packages/plugins/plugin-auth/src/reset-request-mail-after-response.test.ts (+106 -1)" ], "changesets": "@object-ui/auth patch. plugin-auth carries none: its files list is dist and only a test changed.", "bindings_quoted": "One hit in the touched files: forgot-password-success-message.test.tsx header, objectui#4135 the maintainer's 2026-08-11 ruling (single-brace {email} hole). Not moved; the new describe is appended after it and the substitution path is untouched. No hit in ForgotPasswordForm.tsx or the plugin-auth test file.", "policy_unchanged": "reset rule and its budget, the uniform answer, and every door check are as on main.", "deviations": [ "PR carries assignees os-sales: the brief says no assignees, but the maintainer ruling on #29828 (letter A) binds pr_create to carry the claim's Account. Followed the ruling.", "Branch is the harness-assigned claude/project-thread-6kjsre, as the claim names, not claude/issue-32645-slug.", "spec check:generated: check:test-typecheck stopped with tsc failed without diagnostics at the default heap; re-run with NODE_OPTIONS=--max-old-space-size=8192 answers OK. packages/spec is not in this diff.", "The whole plugin-auth suite was run on 3c28e4b5, before the merge of main; after the merge (incoming commits touch neither package) the targeted files and both typechecks were re-run." ], "dogfood_battery": "n/a: no ! in the PR title and no narrowing arm", "mcp_calls": "0: no MCP GitHub tool was called; reads were gh api REST GETs", "api_writes": "2 relay writes: pr_create (fleet-write run 43277965062, PR #32788, body read back identical 12776 bytes) and this os-dev-report comment; plus git push of the branch", "open_questions": [ { "question": "If the hosted reading shows the counter store throwing under a burst, should the rate-limit consume answer a refusal (429) instead of letting the rejection escape as a 500?", "options": [ "A: fail closed: a throwing store answers 429 for that request; the budget is never widened", "B: fall back to the per-node in-process counter for that request; keeps the door open with a per-node budget", "C: leave as is and fix the hosted cache service only" ], "recommendation": "A, because it removes the message-less 500 without admitting anything the budget would refuse; it is a policy change, so it waits for the cloud reading and a ruling, not this PR." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: the seat, which is asking cloud for the production log line and the hosted cache service; noted, not filed" ] }objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-sales · session_01L6bhgWRcTEB3toGxnWvuvX
Maintainer-action: test on production whether the 500 on repeated "forgot password" clicks is the edge rate limiter's answer rather than the framework's — done when a comment on this card records the test result and says whether server work is wanted.
Release: session
session_01L6bhgWRcTEB3toGxnWvuvX· because the maintainer moved the card to themselves 「转维护者处理卡,测试后再开发」 2026-11-12T22:04Z · to the maintainer; a seat re-claims only after the test result is on this card.Ruling-ref: the maintainer judged the server 500 to be the edge rate limit 「#32645 忘记密码连点报 500 你看不到,应该是遇到的 cloudflare 的限流。」 2026-11-12T22:00Z.
Already landed: the client guard, PR #32788 (
Part of #32645). No server change is in flight; this seat writes nothing more on this card until the result arrives.Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
director#1 · hotlong · session_f4c42f815d5e47b39de5e9fb0d6d7864
2026-11-13T00:49Z Maintainer-action done: the production test. The 500 is not the edge rate limiter. It is a leftover physical UNIQUE index on the production control database. No framework server work is wanted for the 500. The fix is cloud's: objectstack-ai/cloud#4439,
priority:p1.Test (production,
cloud.objectos.ai)server-timingpresent), not Cloudflareserver-timing"7 queries"The control-plane container log at the same instant reads:
Insert operation failed {"object":"sys_verification" … duplicate key value violates unique constraint "uniq_sys_verification_value"}, thenDuplicateRecordError … 'Verification Token' already holds this value, thenbetter-auth returned error: 500.Cause
pg_indexeson production's control database still listsuniq_sys_verification_value, a UNIQUE index onvalue.sys-verification.object.ts:153-182saysvaluemust carry no index, and that a unique one breaksoauth2/authorize.valuefor every reset row, so a second live reset row for the same user collides.idx_sys_verification_value, which is why it does not 500 there.Two framework-side notes, for triage to weigh (not filed)
pm:awaiting-maintainer→pm:queue, so triage can decide whether this card closes against cloud#4439 or keeps the hardening note.objectstack-fleet commentedon Nov 13, 2026
repo:cloud#5 · hotlong · session_01Q4EjjPP1zuWBNq3nBXVei9
Pointer for the hosted forgot-password 500: the cause on cloud's production control database is a retired index that objectstack#11701 already removed from the framework. It was never dropped on that database. The cloud fix is objectstack-ai/cloud#4439 (PR objectstack-ai/cloud#4452, merging now): a boot-time drop that runs inside the next approved production release. No framework change is needed.
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: re-scope · p1 → p3 · stays
pm:queue· 2026-11-13T15:28Z⛔ Not a claim.
uniq_sys_verification_valueon the production control database. Per director 6840412288, no framework server work is wanted for it. Do not dispatch this card for the 500.schema sync drop index no longer declared,orphan index schema sync,uniq_sys_verification_value, 0 framework hits):sys_verification.value, which breaks repeated password resets andoauth2/authorize. The framework side should report or drop it on upgrade (for example inos migrate plan) instead of leaving every deployment to find it.