Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-auth): verify-domain answers DOMAIN_VERIFICATION_DISABLED for the disabled condition - #11004
Conversation
…for the disabled condition Both SSO domain-verification bridge routes recognise the same shape - the inner @better-auth/sso endpoint is unmounted for this env and answers 404 with no code - and answered it differently. runRequestDomainVerification returns 400 + DOMAIN_VERIFICATION_DISABLED; runVerifyDomain rewrote only the message and let the code fall through to the generic failure default, so the response carried "the feature is off" copy under a code meaning "verification failed". verify-domain now returns the sibling's answer for that condition: 400 + DOMAIN_VERIFICATION_DISABLED. The status moves with the code because the inner 404 describes the INNER endpoint, while this bridge route is mounted unconditionally. The rewrite stays keyed to the disabled shape (404 WITHOUT a code): a genuine failure still answers DOMAIN_VERIFICATION_FAILED, and the vendor pass-through arm is untouched on both routes. Both directions are pinned. Part of #10859 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
📓 Docs Drift Check1 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 e01f70370f12a1f49189641ae2f8ea8474ad116d && git checkout e01f70370f12a1f49189641ae2f8ea8474ad116d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bd003d821787d3d72e68c5297d3d2088a473c5c0 93c4f7c9b5795e7a3376325f3c913826ee478e5a && git checkout -B drift-repro bd003d821787d3d72e68c5297d3d2088a473c5c0 && git merge --no-ff 93c4f7c9b5795e7a3376325f3c913826ee478e5a
node scripts/docs-audit/affected-docs.mjs --json bd003d821787d3d72e68c5297d3d2088a473c5c0 |
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32560319641 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10859
What each route answered, before and after
Both SSO domain-verification bridge routes recognise the same condition — domain
verification is off for this environment, so
@better-auth/ssonever mounts theinner endpoint and it answers
404with no code — and they answered itdifferently.
POST /admin/sso/request-domain-verification400DOMAIN_VERIFICATION_DISABLEDPOST /admin/sso/verify-domain404DOMAIN_VERIFICATION_FAILED400DOMAIN_VERIFICATION_DISABLEDPOST /admin/sso/verify-domain<inner status>DOMAIN_VERIFICATION_FAILEDPOST /admin/sso/verify-domain<inner status><vendor code>runVerifyDomain's branch for the disabled condition rewrote only themessageand let the code fall through to the route's generic default, so the response
carried "the feature is off" copy under a code that means "verification failed".
A caller can only act on the machine-readable half, and the two halves
disagreed.
Premise re-derived on
origin/mainat047ac86eebefore any edit: it holdsexactly as the card quotes it.
The status question, and the measurement behind it
The card asks whether
verify-domainshould stay404while its siblingrewrites to
400. Answer:400, parity with the sibling. Four measurements,in the order they moved the decision:
header in
register-sso-provider.tssays: "A404from the inner endpointmeans the feature is OFF for this env (endpoints unmounted) → surfaced as
such, not a bare 'not found'." The sibling implements that;
verify-domaindid not.
404was describing the wrong resource. The outer bridge route ismounted unconditionally (
auth-plugin.ts,rawApp.post(.../admin/sso/verify-domain))— it is not behind the feature flag. Only the inner endpoint is unmounted,
so passing its status through said "no such endpoint" about a resource that
demonstrably exists.
answers "capability off":
403(FEEDS_DISABLED),409(FLOW_DISABLED),501(NOT_IMPLEMENTED, admin plugin off),503(OIDC not enabled),404(MCP not enabled),
400(the sibling). There is no house rule to appeal to,so nothing outweighed same-file, same-feature, same-condition parity.
404. The dogfood bucket loop asserts only thatthe admin's status is not in
[401,403]and the code is notPERMISSION_DENIED; the console action runtime uses the HTTP status only asa fallback label when the envelope carries no error. Neither is disturbed
by
400.The status moves with the code deliberately: answering the sibling's code at a
different status would have left "one condition, two answers" half-standing.
Both directions are pinned
packages/plugins/plugin-auth/src/sso-domain-verification-error-codes.test.ts(6 pre-existing tests from #10716, 6 added; all assert
codeandstatusper ADR-0112):
DOMAIN_VERIFICATION_DISABLED+400, on both routes, plus adirect parity assertion that the two routes answer the same code and the
same status for the same condition.
DISABLEDis not stamped on every failure: agenuine uncoded failure still answers
DOMAIN_VERIFICATION_FAILEDwith theinner status passed through, and a
404that carries a vendor code is thevendor's diagnosis, passed through untouched. The rewrite is keyed to the
disabled shape specifically —
404without a code.parsed?.codepass-through arm is untouched on both routes.One fixture was re-fixtured, not just re-spelled. The #10716 test
verify-domain: an uncoded vendor failure answers our SCREAMING defaultwasdriven by
fakeHandle(404, undefined)— which is the disabled shape, not averification failure. Left pointing there it would have kept passing while
testing the wrong branch, and the generic-default arm of
verify-domainwouldhave had no coverage at all. It now drives a real uncoded failure (
502), thesame shape its sibling case uses.
Ablation — prediction written before mutating, both legs
Predictions were recorded to a file before either mutation.
plugin-authhas nodist/and itsvitest.config.tsdeclares no alias, and the test imports thesubject by the relative specifier
./register-sso-provider.js, so the resolvedartifact is
src/and no rebuild step can silently invalidate a leg. Thepositive control for that claim: the suite runs and its result changes from a
src/-only edit, with nodist/present anywhere in the package.expected 'DOMAIN_VERIFICATION_FAILED' to be 'DOMAIN_VERIFICATION_DISABLED'if (resp.status === 404), dropping&& !parsed?.codeexpected 'DOMAIN_VERIFICATION_DISABLED' to be 'NO_PENDING_VERIFICATION'Leg B is the hazard worth naming: an implementation that stamped
DISABLEDonevery
404passes the disabled case and the pre-existing #10716 pass-throughtest (which drives status
400, not404). Only the newly added leg catchesit.
Restores proved byte-identical with
git hash-object, and the ablation markerwas confirmed absent afterwards:
The dogfood note
admin-route-nonadmin-refusal.dogfood.test.ts:261is anote:string —documentation, not an assertion. The bucket loop never reads
note, so nothinghere goes red on its own; it was updated deliberately rather than by being
forced to.
Gates — union derived on the final commit
93c4f7c9b, clean treenode scripts/pm/dispatch-gates.mjswith no path arguments (derived 4 pathsvs merge base
047ac86ee; committed 4, working tree 0, untracked 0). Every exitcode captured before any pipe. All 24 green:
15 derived —
check:changeset-gate-self-tests,spec:check:empty-state,spec:check:liveness,check:objectui-changeset,check:slot-lookup,spec:check:strictness-ledger,check:test-source-alias,check:type-source-resolution,spec:check:variant-docs,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-empty-changeset,check-plugin-teardown-shape,check-affected-docs.5 convention-triggered (this change adds test code) —
check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher,and
check:type-check-debt --re-measure(run against the built workspace closure,turbo run buildexactly aslint.ymldoes: "33 ledger entries re-measured,1908 raw tsc errors total, none above its recorded number").
Plus
check:nul-bytes, and class #10309 run explicitly:check:error-code-casing,check:dispatcher-error-vocabulary,check:route-envelope. The path derivation did not name any of those three —they were run because the class is live, and all three are green on their own
verdict lines (
✓ no unlisted lowercase error codes in 4414 scanned file(s);OK — 21 unregistered code-stamping site(s), all classified;✓ 4 module(s) discovered and audited). No log contains aPREREQUISITE NOT MET/Nothing was checkedrefusal or a zero-match no-op.Package suites:
@objectstack/plugin-authtypecheck exit 0; full suite 67files / 1406 tests passed.
@objectstack/dogfoodtypecheck exit 0.Breaking or not — argued here, not settled here
Shipped as
minor, matching the call the casing rename on these same two routesmade (#10716). For: the emitted vocabulary gains no member (both codes are
already registered for
@objectstack/plugin-auth), and the old pairing wasself-contradictory — a code meaning "verification failed" under a message saying
"the feature is off" — rather than a contract anyone could have relied on
deliberately. Against, stated rather than settled: unlike that rename, whose
old spellings were undeclared values no schema admitted,
DOMAIN_VERIFICATION_FAILEDis a declared, registered code, so a client keyedto it for this case was keyed to something the published contract admitted — and
here both halves of the answer change, code and status. A reviewer who reads
that as
majoris not reading it wrong. This PR does not decide it silently.Scope
No
packages/specchange — none was needed and this lane has none.content/docs/releases/**untouched.#10700is in flight in the same packageon the two-factor surface and was not touched; we share only
.changeset/, onefile each.
of a published endpoint changes),
needs:contract-reviewis the compensatingcontrol on #10859, and this seat does not clear it, mark ready, enable
auto-merge, or merge.
Generated by Claude Code