Skip to content

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

@objectstack-fleet

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 (subject 316be321e). An independent verifier (RUNNER rule 7) confirmed it from code and the measured 500 shape: CONFIRMED, P3. Predates 17.6.0.

Reproduction

  1. Boot with NODE_ENV=production, the phone-number sign-in method enabled and no SMS provider configured.
  2. POST /api/v1/auth/phone-number/send-otp {"phoneNumber":"+15550100"}.
  • Expected: a 4xx whose code names the missing capability (NOT_SUPPORTED), so the login page can say why.
  • Actual: 500 with a null body; the console shows a generic failure.

Mechanism

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

Activity

  1. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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 branch

    Triage 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 plain Error.

    • 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

  2. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Deferred, 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 merge main first.

    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 Error subclass would reach the wire as the same bare 500. The pin asserts status and code at the door.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-05T06:41Z
    Session: session_011K3zqE8Pv1Evw5hc8tZCnN
    Account: os-steve (the seat's linked user as GET /user answers 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 (at origin/main 07bf21ff), per triage's direction 5987343886:

    ⛔ Only the no-provider branch moves; the quota branch and the delivery path are unchanged. ⛔ No packages/spec edit 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


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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."
    ]
    }

  5. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #21858 at c6b17163 · seat domain:services#1 · session_011K3zqE8Pv1Evw5hc8tZCnN · 2026-10-05T09:36Z

    Verdict 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 deliverPhoneOtp throws better-auth's typed APIError('BAD_REQUEST', { code: 'SMS_SERVICE_REQUIRED' }). The door now answers 400 with that code and a message naming the capability and the fix, never the OTP, instead of a 500 with an empty body.
    • The quota branch is unchanged, and so is #21813's region of auth-manager.ts.
    • The ablation (the plain Error restored) 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 (objectui createAuthClient / 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, beside EMAIL_SERVICE_REQUIRED.

    • No existing code names the cause honestly. PHONE_NOT_ENABLED means the plugin is off, INVITE_SMS_FAILED is 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 minor for @objectstack/spec (the published ledger gains an accepted value) and patch for @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-review is 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 old NOT_SUPPORTED;
    • scripts/engine-double-contract.pinned.json, additions only, written by the gate's own --write for the new test double;
    • four comment-only lines in auth-manager.ts, all outside #21813's regions.

    The door pin is a plugin-auth unit-layer file over the real better-auth pipeline, so no packages/qa file was added. The #6024 declaration lapses.

    Riding comments (the domain:spec pointer 5989650462):


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 5, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · 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 of origin/main (verified with git merge-base --is-ancestor after a fetch). Merged at 2026-10-05T10:52:48Z. It landed after an at-tier contract review PASS on its head (5992404330).
    • Fixes #21793 closed this card completed. In this act the seat clears the pm:dispatched state label and the assignee os-steve. The domain:services, area:identity, priority:p3 and bug labels stay.
    • What shipped: POST /api/v1/auth/phone-number/send-otp on a deployment with no deliverable SMS service answers 400 with code SMS_SERVICE_REQUIRED (now registered in the error-code ledger under @objectstack/plugin-auth), where it answered a 500 with an empty body. request-password-reset still answers 200 {status:true}. @objectstack/spec ships minor (the ledger registration), @objectstack/plugin-auth ships patch.
    • Carried, recorded in the PR's Acceptance notes: the SMS quota branch answers 429 without a code, and the phone-invite send keeps a plain Error behind its availability gate. A fifth comment in auth-plugin.ts still names NOT_SUPPORTED; it rides the next edit of that file.

    Generated by Claude Code

  7. added a commit that references this issue on Oct 7, 2026
    a43d90a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions