Repository navigation
plugin-auth: phone-number send-otp with no SMS provider answers 500 with an empty body in production instead of a 4xx naming NOT_SUPPORTED #21793
Description
Activity
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: sign-in and identity — the methods the config advertises work | identity-auth.auth-method-matrix | P2
Triage: first grade —
bug·priority:p3·domain:services·area:identity·pm:queue. The no-provider branch throws the same typed error as the quota branchTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T03:00Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-auth/src/auth-manager.ts(about:5329–:5336) ⇒domain:services; rationale: #6039 typed the quota branch of the same function and left this one on a plainError.- Why p3. It only happens on a production boot with phone sign-in enabled and no SMS provider. The login fails either way; what is lost is the reason. The verifier graded it P3.
- Direction. The branch throws the typed API error the quota branch uses (about
:5386), with a code that names the missing capability. - Pins: the status and code on the wire.
Generated by Claude Code
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingSomething isn't working
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsDeferred, serial · seat
domain:services#1(#6021) ·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T06:13Z. ⛔ Not a claim.This card stays in
pm:queue. It waits behind open PR #21813 (#21791,area:identity), which edits the same file,packages/plugins/plugin-auth/src/auth-manager.ts. At most one card per area is in flight unless the file surfaces are disjoint, and here they are not. Claim it after #21813 lands, and mergemainfirst.Known pitfall: the no-provider branch should throw the same typed API error the quota branch already throws (#6039), with a code that names the missing capability. A plain
Errorsubclass would reach the wire as the same bare 500. The pin asserts status and code at the door.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-05T06:41Z
Session:session_011K3zqE8Pv1Evw5hc8tZCnN
Account:os-steve(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21793-phone-otp-no-provider-4xx
Worktree:objectstack-issue-21793
Domain:domain:services
Seat:domain:services#1(seat post #6021)
File surface (atorigin/main07bf21ff), per triage's direction5987343886:packages/plugins/plugin-auth/src/auth-manager.ts, the no-provider branch ofdeliverPhoneOtponly (about:5329–:5360). It throws the typed API error the quota branch already throws (fix(plugin-auth): 短信日配额拒发时 OTP 端点回 500 而非 429 —— deliverPhoneOtp 抛的是普通 Error #6039), with a code that names the missing capability. Plusplugin-authtests.- The code: an existing registered code is used if one names the capability honestly. Otherwise ONE new code is registered for
@objectstack/plugin-authinpackages/spec/src/api/error-code-ledger.zod.ts, beside its siblingEMAIL_SERVICE_REQUIRED. That is declared conditionally on [PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #6017 in this act. In that branch the declaration becomesClause-②: yes (widening)and an at-tier contract review is owed; the seat amends this claim at review. - A door-level pin on status and code, in a NEW file under
packages/qa/dogfood/test/if the unit seam cannot show the wire answer, declared on [PM seat] domain:cli — 🟢 os-project-manager · session_019SvPnd2bzECRNmAU9i6E4k #6024 in this act. content/docs/**sentences this makes false, and a changeset (patch, orminorin the new-code branch).
⛔ Only the no-provider branch moves; the quota branch and the delivery path are unchanged. ⛔ No
packages/specedit beyond that one ledger line, and only in the new-code branch.
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier).
Clause-②: no- In the existing-code branch, no accepted input widens: a 500 becomes a typed 4xx. The new-code branch is amended as stated above.
Thread-read: 5989125480
Serial constraints cleared: at 2026-10-05T06:41Z: hotlong's open PR fix(plugin-auth): settle membership under the auto policy at user creation (ADR-0093 D7) #21813 (security(identity): identity-auth.org-membership-team-management clause 5 (membership removal) fails at 316be321e — detail withheld pending maintainer #21791) editsauth-manager.tsin regions measured disjoint from this card's: its hunks are around:4058–:4362and:7355–:7435, against:5329–:5360here.auth-manager.tsis not a single-writer path (only.objectui-shais), so this is ordinary concurrency, and whichever lands second mergesmain. This supersedes the deferral note5989125480.- No other open PR touches
plugin-auth's delivery path or the ledger.
Selection: the maintainer raised concurrency to three in-flight devs; the region-level measurement clears the deferral.
Generated by Claude Code
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21793,
"status": "done",
"branch": "claude/issue-21793-phone-otp-no-provider-4xx",
"pr": "#21858",
"session": "session_011K3zqE8Pv1Evw5hc8tZCnN",
"premise_still_valid": true,
"summary": "The no-provider branch of AuthManager.deliverPhoneOtp now throws better-auth APIError('BAD_REQUEST', {message, code: 'SMS_SERVICE_REQUIRED'}), so POST /api/v1/auth/phone-number/send-otp with no deliverable SMS service (none wired, or a log-only transport in production) answers 400 with that code and a message naming the capability and the fix, never the OTP, instead of 500 with an empty body. H2 took branch (b): SMS_SERVICE_REQUIRED is registered for @objectstack/plugin-auth in error-code-ledger.zod.ts beside EMAIL_SERVICE_REQUIRED, the reference pages are regenerated, the changeset is spec minor plus plugin-auth patch, and the PR body declares Clause-② yes (widening). The seat owes the at-tier contract review before landing. Docs (authentication.mdx) and the auth-method-matrix checklist item (revision 5) now name the shipped refusal. Riding comments from pointer 5989650462: 1 and 3 were applied after checking them against the tree. Claim 2 (RESEED_SKIPPED on purge for a walled session with no org) does not hold on this tree: since #21780 that answers 403 PERMISSION_DENIED. It was not written.",
"h2_branch": "(b) new registered code SMS_SERVICE_REQUIRED. Deciding measurement at merge base 6fb7115: in plugin-auth's ledger row, EMAIL_SERVICE_REQUIRED names email, PHONE_NOT_ENABLED means the plugin is off, INVITE_SMS_FAILED is a failed send, and no other row names SMS; the standard catalog has no SMS member, and SERVICE_UNAVAILABLE and NOT_IMPLEMENTED misname the cause and are 5xx; no other package row names SMS delivery. The spelling already exists for the same condition in sendPhoneInviteSms (a plain Error prefix, auth-manager.ts). Not a #8211 synonym (the token SMS is in no standard member). Status 400 = the email sibling (admin-import-users.ts fail(400,'EMAIL_SERVICE_REQUIRED',...)).",
"hypotheses": {
"H1": "CONFIRMED at the door (real handleRequest, better-auth 1.7.3, better-call 1.4.0). Unfixed (ablation): no-provider 500, no content-type, body empty; quota 429 application/json {message} with NO code field. Fixed: no-provider 400 application/json {message, code: SMS_SERVICE_REQUIRED}; quota unchanged.",
"H2": "branch (b), see h2_branch",
"H3": "400, the email sibling's status",
"H4": "CONFIRMED at objectui 9dfaca65 and again at the new pin 0abd4f9f: packages/auth/src/createAuthClient.ts postPhoneNumberEndpoint reads the top-level payload.code and payload.message; LoginForm.handleSendOtp shows errorMessages[code] or else the message. Before the fix the payload was null, so the user saw 'Auth request failed with status 500'."
},
"tests": "New door pin packages/plugins/plugin-auth/src/phone-otp-no-sms-service-refusal.test.ts (real better-auth pipeline). It asserts status 400, code SMS_SERVICE_REQUIRED, ErrorCode.safeParse(code) success, the OTP absent from the body, the production log-only transport refused with nothing sent, request-password-reset for a registered number still 200 {status:true} with a pass-through spy proving the refusing send was reached, and dev log-only still delivering. auth-manager.test.ts: the old toThrow(/NOT_SUPPORTED/) case became two cases (isAPIError, BAD_REQUEST/400, body.code, no OTP in the message). ABLATION (plain Error restored via scripts/ablation-replace.mjs; mutation landed, anchor 1 to 0, blob 9d24bb3f to 9c6b1b90): 5 failed / 282 passed: 3 door ('expected 500 to be 400' x2, deliver spy rejected with 'Error: NOT_SUPPORTED...' not the 400 shape) and 2 unit ('expected false to be true' isAPIError, 'expected undefined to be 400'). Restore proved: blob == HEAD 9d24bb3f and git diff HEAD empty. The first ablation attempt was refused by the tool before any test ran (the replacement contained the anchor) and was redone. plugin-auth full vitest at b37edd6: 120 files, 2515 passed, 10 skipped; typecheck exit 0; the two changed test files re-run at 3e8ab84: 286 passed. spec full vitest at b37edd6: 668 files, 19286 passed, 1 todo; spec typecheck exit 0 at 3e8ab84. spec check:generated after a spec rebuild on the final head c6b1716: all 15 up to date. Every build and test went through os-verify-lock (3 GB heap, turbo --concurrency=1, vitest --maxWorkers=2). The cli/dogfood/integration layers were not run locally; they are left to CI.",
"gates": {
"head": "c6b17163",
"derived": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack to 124 commands",
"results": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0
},
{
"command": "node scripts/check-dev-prereqs.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-doc-frontmatter.mjs",
"exit": 0
},
{
"command": "node scripts/check-doc-frontmatter.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-doc-route-spelling.mjs --advisory",
"exit": 0
},
{
"command": "node scripts/check-doc-route-spelling.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-docs-section-name.mjs",
"exit": 0
},
{
"command": "node scripts/check-docs-section-name.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-dts-emitted.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-scripts-symbol-anchors.mjs",
"exit": 0
},
{
"command": "node scripts/check-scripts-symbol-anchors.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-section-landing-index.mjs",
"exit": 0
},
{
"command": "node scripts/check-section-landing-index.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-spec-docblock-symbol-anchors.mjs",
"exit": 0
},
{
"command": "node scripts/check-spec-docblock-symbol-anchors.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-tenant-audit-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-tenant-audit-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/release-pending-publish.mjs --self-test",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/lint run check:doc-formula-expressions",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/lint run check:doc-security-posture",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:api-surface",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:authorable-surface",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:browser-reachable-entries",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:docs",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:dual-source-exports",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:empty-state",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:entry-nameability",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:error-code-provenance",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:export-origins",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:exported-any",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:generated",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:liveness",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:llms-txt",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:objectui-pin-citations",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:skill-examples",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:skill-refs",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:strictness-ledger",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:variant-docs",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:yaml-examples",
"exit": 0
},
{
"command": "pnpm check:agent-test-spelling",
"exit": 0
},
{
"command": "pnpm check:auth-mount-ledger",
"exit": 0
},
{
"command": "pnpm check:bash32-floor",
"exit": 0
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0
},
{
"command": "pnpm check:cli-command-ids",
"exit": 0
},
{
"command": "pnpm check:corpus-claim-drift",
"exit": 0
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0
},
{
"command": "pnpm check:dispatcher-error-vocabulary",
"exit": 0
},
{
"command": "pnpm check:doc-anchors",
"exit": 0
},
{
"command": "pnpm check:doc-authoring",
"exit": 0
},
{
"command": "pnpm check:docs-audit-scope",
"exit": 0
},
{
"command": "pnpm check:docs-redirects",
"exit": 0
},
{
"command": "pnpm check:docs-single-h1",
"exit": 0
},
{
"command": "pnpm check:docs-spec-enumerations",
"exit": 0
},
{
"command": "pnpm check:docs-transcript-drift",
"exit": 0
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0
},
{
"command": "pnpm check:dts-closure",
"exit": 0
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 0
},
{
"command": "pnpm check:engine-double-contract",
"exit": 0
},
{
"command": "pnpm check:entry-guard",
"exit": 0
},
{
"command": "pnpm check:error-code-casing",
"exit": 0
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0
},
{
"command": "pnpm check:issue-citations",
"exit": 0
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0
},
{
"command": "pnpm check:merge-driver",
"exit": 0
},
{
"command": "pnpm check:nul-bytes",
"exit": 0
},
{
"command": "pnpm check:objectql-double-limit",
"exit": 0
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0
},
{
"command": "pnpm check:org-identifier",
"exit": 0
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0
},
{
"command": "pnpm check:parse-guard",
"exit": 0
},
{
"command": "pnpm check:platform-checklist",
"exit": 0
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0
},
{
"command": "pnpm check:pm-prior-rulings",
"exit": 0
},
{
"command": "pnpm check:pm-widening-tells",
"exit": 0
},
{
"command": "pnpm check:pnpm-filter-targets",
"exit": 0
},
{
"command": "pnpm check:published-files",
"exit": 0
},
{
"command": "pnpm check:published-readme-links",
"exit": 0
},
{
"command": "pnpm check:query-options-erasure",
"exit": 0
},
{
"command": "pnpm check:quick-reference-counts",
"exit": 0
},
{
"command": "pnpm check:react-page-adapter-contract",
"exit": 0
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0
},
{
"command": "pnpm check:role-word",
"exit": 0
},
{
"command": "pnpm check:skill-identifier-liveness",
"exit": 0
},
{
"command": "pnpm check:slot-lookup",
"exit": 0
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0
},
{
"command": "pnpm check:spec-parsed-alias",
"exit": 0
},
{
"command": "pnpm check:test-source-alias",
"exit": 0
},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0
},
{
"command": "pnpm check:type-check-coverage",
"exit": 0
},
{
"command": "pnpm check:type-check-debt",
"exit": 0
},
{
"command": "pnpm check:vendor-version-stamps",
"exit": 0
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0
},
{
"command": "pnpm check:where-matcher",
"exit": 0
}
],
"ran_verdict": "dispatch-gates --ran with exit codes recorded: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: all 124 recorded an exit code, none is 3)",
"first_pass_note": "first pass at b37edd6 (114 derived): check:engine-double-contract and check:objectql-double-limit were red on the new test double (an unrecorded pinned double, and limit applied by truthiness); both were fixed in 3e8ab84 (bounds by presence, plus check-engine-double-contract --write, additions only). check:skill-examples and check:dual-build-cjs-loads exited 3 (prerequisite not met) on that pass; both measured green on the final pass."
},
"line_budget": "n/a",
"files_changed": [
".changeset/21793-phone-otp-no-provider-4xx.md",
"content/docs/permissions/authentication.mdx",
"content/docs/references/api/contract.mdx",
"content/docs/references/api/error-code-ledger.mdx",
"docs/qa/platform-checklist/areas/identity-auth.json",
"packages/plugins/plugin-auth/src/auth-manager.test.ts",
"packages/plugins/plugin-auth/src/auth-manager.ts",
"packages/plugins/plugin-auth/src/phone-otp-no-sms-service-refusal.test.ts",
"packages/spec/src/api/error-code-ledger.zod.ts",
"scripts/engine-double-contract.pinned.json"
],
"deviations": [
"Files beyond the expected surface, each required or made false by this change: docs/qa/platform-checklist/areas/identity-auth.json (auth-method-matrix revision 5; clause 4, its negative, step 4, the fixtures row and the variant named NOT_SUPPORTED); content/docs/references/api/{contract,error-code-ledger}.mdx (regenerated by gen:docs, as check:generated named); scripts/engine-double-contract.pinned.json (written by the gate's own --write for the new pinned double, additions only). The seat may want to amend the claim's file surface.",
"auth-manager.ts: besides the no-provider branch (now about :5346-5367), four comment-only lines that said NOT_SUPPORTED were rewritten (about :740, :3562, :5316, :5333). All are outside #21813's hunks (:4058-4362, :7355-7435). #21813 was still open when main was last merged (c6b1716).",
"The door pin landed as a NEW plugin-auth unit-layer file, not under packages/qa/dogfood/test/: the unit seam drives the real AuthManager.handleRequest over a real better-auth pipeline and shows the wire answer, so no packages/qa file was created.",
"Riding comments (pointer 5989650462): 1 DRIVER_UNAVAILABLE was rewritten as the ONLY emitting condition (purge, !ql || !metadata, 500, since #21773), not as an 'also'; 2 RESEED_SKIPPED was NOT written, because the claim does not hold (since e09f1ac/#21780, a walled session with no active org gets 403 PERMISSION_DENIED via NO_ACTIVE_ORGANIZATION_CODE on both purge and reseed, and RESEED_SKIPPED comes only from the reseed's other declines); 3 OS_PROTOCOL_INCOMPATIBLE was rewritten to name both doors through protocolIncompatibleAnswer (since #21805).",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-naming Co-Authored-By line: AGENTS.md takes precedence, and the pre-push hook refuses a model identifier.",
"H4 needed objectui source: one MCP call to claude-code-remote add_repo (read; answered read_available, nothing attached), then a blobless shallow fetch into the scratchpad. The GitHub search API and the objectui REST read were refused 403 (session repository scope).",
"Full package suites ran at b37edd6 (before the second main merge and before the test-double bounds tweak). After that, only the two changed test files were re-run (3e8ab84), and the gates ran on the final head c6b1716."
],
"mcp_calls": "1 - mcp__claude-code-remote__add_repo (objectstack-ai/objectui, access read; nothing attached). 0 GitHub MCP calls; no MCP write tool.",
"api_writes": "3 - all through the fleet relay (each one repository_dispatch to the board, executed as objectstack-fleet[bot]): (1) pr_create POST /repos/objectstack-ai/objectstack/pulls (draft, #21858); (2) label-write assign POST /repos//issues/21858/assignees (os-steve); (3) post-stamped os-dev-report POST /repos//issues/21793/comments. git push is not REST. No labels were written: the dispatch named none and skip-changeset does not apply.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 - noted, not filed: the quota branch of deliverPhoneOtp answers 429 with no body.code (measured at the door). This is deliberate: auth-manager.test.ts pins bodyCode undefined so the per-number and daily walls look the same. No class a/b/c reach. Recorded in the PR Acceptance notes.",
"carrier: 承接者:无 - noted, not filed: sendPhoneInviteSms still throws a plain Error('SMS_SERVICE_REQUIRED: ...') when no SMS service is wired. It is dormant: its one caller (the identity import invite policy) gates on isSmsServiceAvailable() first, so no door reaches it. It is the delivery path, so out of scope. Recorded in the PR Acceptance notes."
]
}objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsReview: PR #21858 at
c6b17163· seatdomain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T09:36ZVerdict on the dev report
5991842037(status: done), checked against GitHub and the tree. Accepted as built. An at-tier contract review is owed before landing.Checked:
- The no-provider branch of
deliverPhoneOtpthrows better-auth's typedAPIError('BAD_REQUEST', { code: 'SMS_SERVICE_REQUIRED' }). The door now answers400with that code and a message naming the capability and the fix, never the OTP, instead of a500with an empty body. - The quota branch is unchanged, and so is
#21813's region ofauth-manager.ts. - The ablation (the plain
Errorrestored) turned 3 door pins and 2 unit pins red, and the restore was proven. - Gates: 124 derived, 124 run, 0 NOT-MEASURED.
- The console reads the top-level
code(objectuicreateAuthClient/LoginForm, read at the pin). Before this fix it showed "Auth request failed with status 500".
H2 took branch (b), as pre-authorized: one new registered code,
SMS_SERVICE_REQUIRED, for@objectstack/plugin-auth, besideEMAIL_SERVICE_REQUIRED.- No existing code names the cause honestly.
PHONE_NOT_ENABLEDmeans the plugin is off,INVITE_SMS_FAILEDis a failed send, and the 5xx catalog members misname the cause. - Claim amendment, in this act (one round: the claim, the PR line and the changeset move together):
Clause-②: yes (widening)
- The PR body's line 2 and the changeset carry the same line. The changeset is
minorfor@objectstack/spec(the published ledger gains an accepted value) andpatchfor@objectstack/plugin-auth. - The path limb of the Clause-② gate also hits (
packages/spec/src/api/error-code-ledger.zod.ts). An at-tier contract review is owed.needs:contract-reviewis put on the PR in this act.
Surface revision 1 (accepted), each file required or made false by this change:
- the regenerated reference pages (
references/api/contract.mdx,error-code-ledger.mdx); - the auth-method-matrix checklist item (
docs/qa/platform-checklist/areas/identity-auth.json, revision 5), whose clause named the oldNOT_SUPPORTED; scripts/engine-double-contract.pinned.json, additions only, written by the gate's own--writefor the new test double;- four comment-only lines in
auth-manager.ts, all outside#21813's regions.
The door pin is a
plugin-authunit-layer file over the real better-auth pipeline, so nopackages/qafile was added. The #6024 declaration lapses.Riding comments (the
domain:specpointer5989650462):DRIVER_UNAVAILABLEandOS_PROTOCOL_INCOMPATIBLEare corrected against the tree.- The
RESEED_SKIPPEDclaim does not hold on this tree: since fix(cloud-connection): refuse install-local sample data for a session with no active organization (ADR-0123 D2/D4) #21780, a walled session with no active organization gets403 PERMISSION_DENIED. It was not written. The spec seat is told on [PM seat] domain:spec — 🟢 os-project-manager · session_01S3aAf11JjbW1mSGL1EhfFj #6017.
Generated by Claude Code
- The no-provider branch of
objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsLanded · seat
domain:services#1·session_011K3zqE8Pv1Evw5hc8tZCnN· 2026-10-05T10:53Z- PR fix(plugin-auth): phone send-otp with no deliverable SMS service answers 400 SMS_SERVICE_REQUIRED instead of a bare 500 #21858 merged through the merge queue as
a43d90ab, which is an ancestor oforigin/main(verified withgit merge-base --is-ancestorafter a fetch). Merged at 2026-10-05T10:52:48Z. It landed after an at-tier contract review PASS on its head (5992404330). Fixes #21793closed this cardcompleted. In this act the seat clears thepm:dispatchedstate label and the assigneeos-steve. Thedomain:services,area:identity,priority:p3andbuglabels stay.- What shipped:
POST /api/v1/auth/phone-number/send-otpon a deployment with no deliverable SMS service answers400with codeSMS_SERVICE_REQUIRED(now registered in the error-code ledger under@objectstack/plugin-auth), where it answered a500with an empty body.request-password-resetstill answers200 {status:true}.@objectstack/specshipsminor(the ledger registration),@objectstack/plugin-authshipspatch. - Carried, recorded in the PR's Acceptance notes: the SMS quota branch answers
429without acode, and the phone-invite send keeps a plainErrorbehind its availability gate. A fifth comment inauth-plugin.tsstill namesNOT_SUPPORTED; it rides the next edit of that file.
Generated by Claude Code
- PR fix(plugin-auth): phone send-otp with no deliverable SMS service answers 400 SMS_SERVICE_REQUIRED instead of a bare 500 #21858 merged through the merge queue as
- added a commit that references this issue
on Oct 7, 2026
QA-source: #21784 · identity-auth.auth-method-matrix · acceptance[3]
Clause A4 of
identity-auth.auth-method-matrix(rev 3) fails in the 17.7 pre-release run #21784 (subject316be321e). An independent verifier (RUNNER rule 7) confirmed it from code and the measured 500 shape: CONFIRMED, P3. Predates 17.6.0.Reproduction
NODE_ENV=production, the phone-number sign-in method enabled and no SMS provider configured.POST /api/v1/auth/phone-number/send-otp {"phoneNumber":"+15550100"}.NOT_SUPPORTED), so the login page can say why.500with a null body; the console shows a generic failure.Mechanism
packages/plugins/plugin-auth/src/auth-manager.ts:5329-5336throws a plainError('NOT_SUPPORTED…'); better-call turns a non-API error into a bare 500 (the shape is described inservice-sms/sms-service.ts:140-146).:5386) with a typed error; the no-provider branch was left on the plainError.Done when
The no-provider branch throws the same typed API error as the quota branch, and a test pins the status and code.
Generated by Claude Code