Skip to content

fix(plugin-auth): answer a registered ADR-0112 code from the SSO domain-verification routes - #10858

Merged
huangyiirene merged 1 commit into
mainfrom
claude/issue-10716-sso-error-code-casing
Aug 22, 2026
Merged

fix(plugin-auth): answer a registered ADR-0112 code from the SSO domain-verification routes#10858
huangyiirene merged 1 commit into
mainfrom
claude/issue-10716-sso-error-code-casing

Conversation

@os-warren

@os-warrenos-warren commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10716

What

packages/plugins/plugin-auth/src/register-sso-provider.ts shapes both SSO
domain-verification failures as code: parsed?.code || OUR_DEFAULT. The two
halves have different owners, and only one of them is ours:

  • parsed?.code — @better-auth/sso's own code, passing through. Untouched by this PR.
  • the default — authored by ObjectStack, so ADR-0112 D1 applies to it. Both were lowercase.

Reproduced on this branch at base 5f2e54cc6 before editing (the card measured them at 47aff0938):

linewasis
411parsed?.code || 'request_domain_verification_failed'parsed?.code || 'DOMAIN_VERIFICATION_FAILED'
465parsed?.code || 'verify_domain_failed'parsed?.code || 'DOMAIN_VERIFICATION_FAILED'

The ledger question: reused, not registered — and packages/spec is untouched

DOMAIN_VERIFICATION_FAILED is already registered for @objectstack/plugin-auth
in packages/spec/src/api/error-code-ledger.zod.ts:341. Nothing is added to the
vocabulary and this PR touches no spec file.

The alternative — registering REQUEST_DOMAIN_VERIFICATION_FAILED /
VERIFY_DOMAIN_FAILED, a pure casing rename preserving every current
distinction — needs a packages/spec ledger entry, which is outside this lane.
It is written up as an open question below rather than decided here, because a
wire value should break once: this PR is draft and needs:contract-review
precisely so that choice can be redirected before it lands.

Measured, not assumed — why an unregistered SCREAMING spelling was not an option:

probe: both literals renamed to REQUEST_DOMAIN_VERIFICATION_FAILED / VERIFY_DOMAIN_FAILED
check-dispatcher-error-vocabulary -> exit 0, "21 unregistered code-stamping site(s)" (baseline: 21)
positive control: ONE DIRECT unregistered code in the same file
check-dispatcher-error-vocabulary -> exit 1, "[unclassified-site] ... stamps unregistered code
'ZZ_POSITIVE_CONTROL_UNREGISTERED' (objlit)", 22 sites found

The file is in scan; the gate simply cannot see the fallback slot — the same
blindness #10658 just fixed in the casing gate, filed already as #10762 (I added
this positive control there). So an unregistered code in that slot would have
shipped invisible to both error-code gates: the silent fourth state ADR-0112
D3 exists to prevent. Trading a visible casing violation for an invisible
registration violation is not a fix.

Pins — both directions, because one direction is not a pin

New: packages/plugins/plugin-auth/src/sso-domain-verification-error-codes.test.ts (6 tests).

#directionassertion
our default reaches the wireuncoded vendor failure ⇒ DOMAIN_VERIFICATION_FAILED, both routes
vendor pass-through preservedvendor NO_PENDING_VERIFICATION ⇒ returned unchanged, both routes
statuses unchangedresp.status passes through on every path (502, 404, 400)
reuse is mechanicalthe emitted code is toContain-ed in ERROR_CODE_LEDGER['@objectstack/plugin-auth'], read through the published @objectstack/spec/api exports
the sibling code is not blurredrequest-domain-verification's disabled branch still answers DOMAIN_VERIFICATION_DISABLED / 400

② is the load-bearing one: a handler that stamped our code unconditionally would
pass ① while destroying the vendor's diagnosis. Proven by ablation, not asserted.

Ablation

Predictions written before mutating (ablation-prediction.txt), both legs run with
no rebuild, both restores git hash-object-identical to 62e381c3ab93f937281828cc0df1e6bed922b697:

legmutationpredictedobserved
Adefault reverted to verify_domain_failed1 fail: "answers our SCREAMING default"expected 'verify_domain_failed' to be 'DOMAIN_VERIFICATION_FAILED', 1 failed / 5 passed
Bvendor arm dropped (unconditional overwrite)1 fail: "vendor code reaches the caller unchanged"expected 'DOMAIN_VERIFICATION_FAILED' to be 'NO_PENDING_VERIFICATION', 1 failed / 5 passed

Both restore legs re-ran 6/6 green. Resolution is proven, not asserted: a
src/-only edit flipped the result in both directions with no build step, so the
suite reads src/register-sso-provider.ts. The ledger half resolves through
dist — measured separately: ERROR_CODE_LEDGER imported from the package root
@objectstack/spec is undefined, from @objectstack/spec/api it is an object
with 22 owner keys.

Coordination with #10658 — I landed second

#10658 is closed; its gate PR #10760 is merged. Its KNOWN_LOWERCASE_CODES
registry carried both of my codes, shrink-only, and its own self-test pins that a
landed rename goes STALE and fails. Measured on the probe above:

✗ 2 stale KNOWN_LOWERCASE_CODES entry/entries: ... — no longer present

So this PR deletes both entries, per the "whichever lands second" agreement. The
gate now reports zero exceptions:

before: · 2 known lowercase code(s) deferred to their owning card (KNOWN_LOWERCASE_CODES)
after: · 0 known lowercase code(s) deferred to their owning card (KNOWN_LOWERCASE_CODES)

Emptying the list would have taken its self-test coverage with it (all four registry
cases key on the two entries), so the registry is now injectable and those cases
drive a fixture map — the shrink-only semantics outlive the live list reaching zero —
plus one new case pinning the live list at zero, since the list is closed to new
entries by its own documented rule. Reviewers who consider that pin over-reach for
this lane: say so and I will drop it.

Dogfood pin (admin-route-nonadmin-refusal.dogfood.test.ts:252)

Updated to DOMAIN_VERIFICATION_FAILED; expected value only, no refactor.

⚠️ Correction to a premise the card inherited from #10658: that line is a note:
string — documentation, not an assertion. The bucket loop asserts
admin.code).not.toBe('PERMISSION_DENIED') and never reads note. So the rename
would not have reddened it; it would have gone quietly wrong. Updating it in this
PR is still right, for the reason the card gave.

⚠️ Known textual overlap with PR #10800 (card #10349), which holds the
better-auth-gate bucket in this same file. Different region, deliberate on both
sides, whichever lands second resolves it. Not semantic: my edit changes one
string in a route the other PR does not touch.

Changeset

minor for @objectstack/plugin-auth, with the FROM → TO table an upgrading
client greps for. It does not carry a breaking declaration, and that is a
judgement worth a reviewer's eye — the gate records it in the open:

✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).

Reasoning: the declared wire contract is error.code ∈ StandardErrorCode ∪ ERROR_CODE_LEDGER,
and neither lowercase spelling was ever a member — they were undeclared values a
blind gate let through, so this brings the implementation onto the published
contract rather than changing it. Measured support: zero consumers of either
spelling anywhere in objectstack, objectui or objectos (outside the emitter,
the gate's own fixtures and that dogfood note).

The honest alternative is not available in this lane, and that is the real reason
to look: declaring breaking obliges exactly one ADR-0087 disposition marker, and
every category is either false here (unpublished — plugin-auth is published;
already-registered — no such id; no-migration-prescription and
runtime-interface-only — both refused, since the body carries a FROM → TO
prescription) or requires the registered marker naming a migration id, i.e. a
new entry in packages/spec/src/migrations/registry.ts. If contract review
reads this as breaking, this PR cannot carry that alone
— it needs a spec-lane
follow-up before merge. The disposition is written out in prose in the changeset
body meanwhile, so the question is answered in writing even where the marker
vocabulary has no slot for it.

Verification

Union derived after the final commit on a clean tree, node scripts/pm/dispatch-gates.mjs
with no path arguments, at d4005cec7. Exit codes captured before any pipe.

gateverdict line
check:error-code-casing✓ no unlisted lowercase error codes in 4369 scanned file(s) (ADR-0112). + 29 recognizer case(s) + 5 registry case(s) pass
check:route-envelope (#10309)✓ Plugin-mounted Hono routes — 12 module(s) audited … 8 conformant, 0 ratcheted, 3 exempt, 1 vendor-wire
check:dispatcher-error-vocabulary (#10309)OK — 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).
check-adr-0087-registration✓ … no declared-breaking changeset (1 non-breaking changeset(s) seen).
check-changeset-no-major✓ This diff introduces no `major` bump.
check-empty-changeset✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
check:changeset-gate-self-tests✓ 212 assertions over real temp git repos + ✓ 116 assertions
check:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared
check:objectui-changeset✓ objectui-changeset-digest --self-test: all checks passed
check:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) … none new
check:test-source-aliasOK — 72 packages with tests scanned
check:type-source-resolutionOK — 76 packages with a tsconfig.json scanned
check-plugin-teardown-shape✓ 54 Plugin implementation(s) across 4384 source(s)
check-affected-docs✓ affected-docs self-test: 308 cases pass.
check:query-options-erasure✓ ratchet holds: 67 unswept non-test site(s) … none new
check:engine-double-contractOK — 371 pinned, 133 in the DEBT ledger, 2 exempt.
check:where-matcher✓ conformance holds: 272 matcher(s) discovered
check:type-check-coverageOK — 64/77 workspace packages type-checked … 13 in the DEBT ledger
@objectstack/plugin-authtestTest Files 64 passed (64) · Tests 1357 passed (1357)
@objectstack/plugin-authtypecheckexit 0

check:route-envelope and check:dispatcher-error-vocabulary were not named by
the derivation
— they were run because the card named the #10309 class, and they
are the two most relevant gates to an error-vocabulary change.

Two declared narrowings, both re-run by CI regardless:

  • check:type-check-debt --re-measure needs the whole workspace built. Instead I
    measured the only thing that could move it: plugin-auth's TEST_DEBT program with
    the **/*.test.ts exclusion lifted reports 0 errors naming the new test file,
    so the ratchet cannot drift up. The ledger entry (109) is untouched and --lower
    was not run.
  • @objectstack/dogfoodtypecheck exits 2 with 227 TS2307 Cannot find module
    from an unbuilt dependency closure (@objectstack/verify, plugin-audit,
    plugin-webhooks, …) — none at line 252, and a one-word change inside a string
    literal cannot produce TS2307.

Not decided here

  • Reuse vs. registering two codes (above) — a packages/spec call.
  • The ledger annotates DOMAIN_VERIFICATION_FAILED as "pass-through from
    better-auth"; after this PR we author it too. Comment only, packages/spec, not
    touched — filed as a finding.
  • verify-domain answers a failure code when the feature is disabled, while
    its sibling answers DOMAIN_VERIFICATION_DISABLED for the same condition. That
    incoherence predates this PR (the old bespoke code said "failed" too) and fixing
    it is a behaviour change, not a casing fix — filed separately.

Generated by Claude Code

…in-verification routes
`request-domain-verification` and `verify-domain` shape their failure as
`code: parsed?.code || <our default>`. The `parsed?.code` half is
@better-auth/sso's own code passing through; the default half is ours, and both
defaults were lowercase (`request_domain_verification_failed`,
`verify_domain_failed`) — an ADR-0112 D1 violation that the casing gate could
not see, because a literal reached through an `||` chain matched none of its
recognizers until #10658 widened them.
Both defaults now answer `DOMAIN_VERIFICATION_FAILED`, already registered for
`@objectstack/plugin-auth` in the error-code ledger. Reused rather than
invented: a new spelling would need a `packages/spec` registration to be a legal
`error.code`, and — measured here — an unregistered code in a fallback slot is
invisible to both error-code gates, so it would have shipped as the silent
fourth state ADR-0112 D3 exists to prevent.
The vendor pass-through arm is untouched, and that is pinned in both directions:
a handler that stamped our code unconditionally would pass a suite that only
pinned "our code appears" while destroying the vendor's diagnosis.
Also removes both `KNOWN_LOWERCASE_CODES` entries from
`scripts/check-error-code-casing.mjs` — the coordination #10658 asked of
whichever side landed second, so the gate ends green with zero exceptions. Its
registry self-test now drives a fixture registry (the shrink-only semantics
outlive the live list reaching zero) and pins the live list AT zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

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. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 11 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 db82944559893eda5e0eb171c8bdf9fff23638a7packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0d858c3d61fde8e292fe875714c37ca520c81a4f — the merge of head d4005cec7f8e45fe14f6b874fb7ce5cb4b83792a into base db82944559893eda5e0eb171c8bdf9fff23638a7, 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 0d858c3d61fde8e292fe875714c37ca520c81a4f && git checkout 0d858c3d61fde8e292fe875714c37ca520c81a4f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin db82944559893eda5e0eb171c8bdf9fff23638a7 d4005cec7f8e45fe14f6b874fb7ce5cb4b83792a && git checkout -B drift-repro db82944559893eda5e0eb171c8bdf9fff23638a7 && git merge --no-ff d4005cec7f8e45fe14f6b874fb7ce5cb4b83792a
node scripts/docs-audit/affected-docs.mjs --json db82944559893eda5e0eb171c8bdf9fff23638a7

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

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32543523541 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.01s
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 59 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-warren@huangyiirene