Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-auth): answer a registered ADR-0112 code from the SSO domain-verification routes - #10858
Conversation
…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
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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 |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32543523541 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10716
What
packages/plugins/plugin-auth/src/register-sso-provider.tsshapes both SSOdomain-verification failures as
code: parsed?.code || OUR_DEFAULT. The twohalves have different owners, and only one of them is ours:
parsed?.code— @better-auth/sso's own code, passing through. Untouched by this PR.Reproduced on this branch at base
5f2e54cc6before editing (the card measured them at47aff0938):parsed?.code || 'request_domain_verification_failed'parsed?.code || 'DOMAIN_VERIFICATION_FAILED'parsed?.code || 'verify_domain_failed'parsed?.code || 'DOMAIN_VERIFICATION_FAILED'The ledger question: reused, not registered — and
packages/specis untouchedDOMAIN_VERIFICATION_FAILEDis already registered for@objectstack/plugin-authin
packages/spec/src/api/error-code-ledger.zod.ts:341. Nothing is added to thevocabulary and this PR touches no spec file.
The alternative — registering
REQUEST_DOMAIN_VERIFICATION_FAILED/VERIFY_DOMAIN_FAILED, a pure casing rename preserving every currentdistinction — needs a
packages/specledger 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-reviewprecisely so that choice can be redirected before it lands.
Measured, not assumed — why an unregistered SCREAMING spelling was not an option:
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).DOMAIN_VERIFICATION_FAILED, both routesNO_PENDING_VERIFICATION⇒ returned unchanged, both routesresp.statuspasses through on every path (502, 404, 400)toContain-ed inERROR_CODE_LEDGER['@objectstack/plugin-auth'], read through the published@objectstack/spec/apiexportsDOMAIN_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 withno rebuild, both restores
git hash-object-identical to62e381c3ab93f937281828cc0df1e6bed922b697:verify_domain_failedexpected 'verify_domain_failed' to be 'DOMAIN_VERIFICATION_FAILED', 1 failed / 5 passedexpected 'DOMAIN_VERIFICATION_FAILED' to be 'NO_PENDING_VERIFICATION', 1 failed / 5 passedBoth 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 thesuite reads
src/register-sso-provider.ts. The ledger half resolves throughdist— measured separately:ERROR_CODE_LEDGERimported from the package root@objectstack/specisundefined, from@objectstack/spec/apiit is an objectwith 22 owner keys.
Coordination with #10658 — I landed second
#10658 is closed; its gate PR #10760 is merged. Its
KNOWN_LOWERCASE_CODESregistry 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:
So this PR deletes both entries, per the "whichever lands second" agreement. The
gate now reports zero exceptions:
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.note:string — documentation, not an assertion. The bucket loop asserts
admin.code).not.toBe('PERMISSION_DENIED')and never readsnote. So the renamewould not have reddened it; it would have gone quietly wrong. Updating it in this
PR is still right, for the reason the card gave.
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
minorfor@objectstack/plugin-auth, with the FROM → TO table an upgradingclient 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:
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,objectuiorobjectos(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-prescriptionandruntime-interface-only— both refused, since the body carries a FROM → TOprescription) or requires the
registeredmarker naming a migration id, i.e. anew entry in
packages/spec/src/migrations/registry.ts. If contract reviewreads 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.mjswith no path arguments, at
d4005cec7. Exit codes captured before any pipe.check:error-code-casing✓ no unlisted lowercase error codes in 4369 scanned file(s) (ADR-0112).+29 recognizer case(s) + 5 registry case(s) passcheck:route-envelope(#10309)✓ Plugin-mounted Hono routes — 12 module(s) audited … 8 conformant, 0 ratcheted, 3 exempt, 1 vendor-wirecheck: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 assertionscheck:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declaredcheck:objectui-changeset✓ objectui-changeset-digest --self-test: all checks passedcheck:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) … none newcheck:test-source-aliasOK — 72 packages with tests scannedcheck:type-source-resolutionOK — 76 packages with a tsconfig.json scannedcheck-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 newcheck:engine-double-contractOK — 371 pinned, 133 in the DEBT ledger, 2 exempt.check:where-matcher✓ conformance holds: 272 matcher(s) discoveredcheck: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-authtypecheckcheck:route-envelopeandcheck:dispatcher-error-vocabularywere not named bythe 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-measureneeds the whole workspace built. Instead Imeasured the only thing that could move it: plugin-auth's TEST_DEBT program with
the
**/*.test.tsexclusion lifted reports 0 errors naming the new test file,so the ratchet cannot drift up. The ledger entry (109) is untouched and
--lowerwas not run.
@objectstack/dogfoodtypecheckexits 2 with 227TS2307 Cannot find modulefrom an unbuilt dependency closure (
@objectstack/verify,plugin-audit,plugin-webhooks, …) — none at line 252, and a one-word change inside a stringliteral cannot produce TS2307.
Not decided here
packages/speccall.DOMAIN_VERIFICATION_FAILEDas "pass-through frombetter-auth"; after this PR we author it too. Comment only,
packages/spec, nottouched — filed as a finding.
verify-domainanswers a failure code when the feature is disabled, whileits sibling answers
DOMAIN_VERIFICATION_DISABLEDfor the same condition. Thatincoherence 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