Skip to content

docs(adr-0135): with SSO domain verification on, a provider creates an account only at a domain it has proven - #32951

Merged
os-zhuang merged 9 commits into
mainfrom
claude/project-thread-ab4lnt
Nov 13, 2026
Merged

os-zhuang merged 9 commits into
mainfrom
claude/project-thread-ab4lnt

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Supersedes #32638.

维护者速读

What changes

docs/adr/0135-identity-and-access-architecture.md, the "Domain verification is opt-in" paragraph, states what verification guarantees:

  • the DNS-TXT proof covers every domain a provider lists, a provider signs nobody in until that list is proven, and editing the list clears the proof;
  • with verification on, where the platform admits a new account, a provider creates an account only for an email address at a domain it has proven, compared exactly (a subdomain counts only when it is itself listed and verified, and a listed entry counts only for the host the provider proved), failing closed, and relaxed by no audience posture, invitation or JIT toggle; accounts that already exist are not re-checked;
  • with the rule that a domain is claimed by one provider at most, held where a provider is registered and where its domain list is edited, an SSO provider creates a new account at a domain only when it is the one provider that claims that domain and has proven it.

The two rule sentences carry symbol anchors (ADR-0135 Provenance point 2): packages/plugins/plugin-auth/src/sso-account-at-proven-domain.ts#refuseSsoAccountOutsideProvenDomain and packages/plugins/plugin-auth/src/sso-provider-lifecycle.ts#findSsoDomainClaim.

Whose rule this is. The exact comparison and the "no posture, invitation or JIT toggle relaxes it" clause are a rule made in this repository, not text carried from cloud ADR-0024. The ADR now says so in a dated note under D6, and "What this record does NOT settle" names that note as the one exception to "this file adds no clause". The maintainer ruled on it on 2026-11-13 (approved, no carve-out; a domain entry counts only when written as a bare host), and the note records that ruling and names #32669 as the PR that implements it.

The code that holds the rule, its tests, the code comments and content/docs/permissions/sso.mdx land in #32669. This PR is separate because docs/adr/** is a Tier H governed surface.

Tests

None: documentation only.

Acceptance notes

Verification

At head 54b1620a02 (5ad86211ce with origin/main merged in, then the anchor commit 4b57a83691, the delta-review fix 0815aea4b7, and the ruling note), node scripts/pm/dispatch-gates.mjs --commands derived 21 families from the 1 changed path; each was run with its exit captured before any pipe.

Gates
  • 20 of 21 exit 0, among them check:adr-anchors, check:adr-index, check:adr-links, check:doc-authoring, check:nul-bytes, check:pm-governed-merges, check:pm-prior-rulings, and @objectstack/lint check:doc-formula-expressions (after building the lint closure).
  • pnpm check:adr-symbol-anchors exit 1, one finding: [unresolved-path] for sso-account-at-proven-domain.ts#refuseSsoAccountOutsideProvenDomain, the intended red described above. The findSsoDomainClaim anchor resolves.
  • dispatch-gates --ran: 21 derived, 21 run, 0 NOT-MEASURED, 0 UNRUN.

Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added documentation Improvements or additions to documentation skip-changeset PR has no user-facing published change; bypasses the changeset gate labels Nov 13, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 4b57a83 · 2026-11-13T00:19Z

Delta review of the FAIL on #32638 (record 6839445979: M1, S1) plus a full governed-text review of this head; the lane seat takes it (rules face, CONTRACT_REVIEW_TIER).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4b57a83691b9a020e23c97295bef963ea5515e57
Local-runs: none
Scope: docs/adr/0135-identity-and-access-architecture.md + none

The face is rules: one governed path. This is a full review of this head, and the delta on record 6839445979 (#32638: M1, S1). Read: this PR's body, its 1-file list and its net diff against main (12 lines added, 4 removed, one paragraph); #32638's body and its 5 comments; and the 54 check-runs on this head. Read and not reviewed: the first consumer, #32669 at ec3a30773b (body, 10-file list, its non-test diff and the rule module in full); sso-provider-lifecycle.ts and auth-manager.ts on main; and the published @better-auth/sso 1.7.12, the version this tree pins.

① Derived judgments

Each change the diff implies, named right or wrong.

  1. No accept set and no public surface moves: one paragraph of a governed record changes. Right.
  2. Removed: the sentence that names an organization admin as the registrant. Right: on main, /sso/register admits platform administrators only.
  3. Added: the proof covers every domain a provider lists; a provider signs nobody in until the list is proven; editing the list clears the proof. Right against @better-auth/sso 1.7.12 under this tree's sso() options: verify-domain reads every parsed entry of the list, the sign-in door and both callback paths refuse a provider that is not verified, and update-provider resets the flag when the stored domain value changes.
  4. Added: the proven-domain rule for a new account. Right against fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669 at ec3a30773b, clause by clause: judged at user.validateUserInfo for create-user from an sso-oidc or sso-saml source; equality in one spelling; an entry counted only when written as the bare host; refused on no engine, no row, another provider's row, a failed read, or a flag that does not read on; judged before the linking and audience gates, so no posture, invitation or allowJit reaches it.
  5. Added: accounts that already exist are not re-checked. Right: the rule answers nothing for any action but create-user.
  6. Added: the sentence resting on "a domain is claimed by one provider at most". Right as far as that rule reaches: findSsoDomainClaim on main refuses a second claim at /sso/register and at a domain edit, and both rules read a list entry through the same providerDomains. Its bound is N2.
  7. Added: two symbol anchors. Right. sso-provider-lifecycle.ts#findSsoDomainClaim resolves on main. sso-account-at-proven-domain.ts#refuseSsoAccountOutsideProvenDomain names the path fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669 adds and a declaration at its head; it does not resolve at this head, by design (see ④).
  8. Kept: off by default. Right: the toggle resolves to false when neither config nor environment sets it.

② Semver level

The skip-changeset label and no changeset. It matches what the diff publishes: one file under docs/adr/**, and no package releases. The body carries no Clause-②: line and none is owed: nothing published widens or narrows, and the face is decided by the governed path. Check Changeset and PR-body guards concluded success. Right.

③ Boundary flags

Check-runs on this head: 54, all completed; 24 success, 28 skipped, 2 failure. The failures are Lint · docs & code guards (52 gates: 51 passed, 1 red) and its roll-up Lint & Repo Gates. The one red gate is "ADR symbol anchors resolve", with one finding: [unresolved-path] at line 265 for the first anchor. Nothing else is red.

④ Design judgment

The delta on record 6839445979.

  • M1: resolved. The rule sentence carries packages/plugins/plugin-auth/src/sso-account-at-proven-domain.ts#refuseSsoAccountOutsideProvenDomain, the anchor that record named. The file's Provenance promise (every mechanism claim anchored into this tree) is met for it, and the tie between the sentence and its code is now read by a gate.
  • S1: resolved. The claim sentence carries packages/plugins/plugin-auth/src/sso-provider-lifecycle.ts#findSsoDomainClaim, which resolves on main.

The delta between 5ad86211ce and this head, in the governed file, is those two anchors and nothing else.

Is the red right? Yes. The gate reports a true fact about this head: the tree does not hold the code the sentence describes. It clears with no edit to this PR once #32669 is on main and main is merged in, and it runs on merge_group, so the order holds at the queue as well as on the PR. No anchor-exempt marker is used, and none should be. Two conditions follow. This record read #32669 at ec3a30773b: if #32669 lands from another head, the four rule clauses are re-read against what landed. And the green head that follows is a new head, which the seat accounts for under the head-move rule.

The ADR against its first consumer. The 5 changed sentences were read against #32669 and the vendor code, as ① lists, and each holds. verify-domain proves, for each entry, the host the vendor's own parser reads from it; #32669 counts an entry only when it is already written as a bare host in its one spelling. So "counts only for the host the provider proved" holds as the upper bound it states: an entry counts for that host or for nothing. For an internationalised entry this rests on the vendor's host parser (tldts), which was not read.

The ADR against its own file.

No must-fix. The delta owed is S2: one note, or one clause, in the same paragraph.

Implemented-by: claude/project-thread-ab4lnt
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

VERDICT: PASS

…y's own, and state its condition and the claim rule's bound

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1fyAgM5kyVcE6pbx2
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 0815aea · 2026-11-13T00:42Z

Delta review of S2, N1, N2 (record 6840140663) by the same reviewer.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0815aea4b701eef3e726ec24b8a5f18d3508e3eb
Local-runs: none
Scope: docs/adr/0135-identity-and-access-architecture.md + none

This is the delta on record 6840140663 (head 4b57a83691: S2, N1, N2). Read: the diff between 4b57a83691 and this head (one commit, one file, 17 lines added, 7 removed), this PR's refreshed body and its 1-file list, and the 48 check-runs on this head. The code read for record 6840140663 was not re-read, except auth-manager.ts and register-sso-provider.ts on main for the one new mechanism phrase. Where this record is brief, that record's ①–④ stand.

① Derived judgments

  1. No accept set and no public surface moves: the delta is prose in one governed record. Right.
  2. Changed: "With it on" now opens the rule sentence, and the last sentence names its subject, "Domain verification is off by default". Right: the rule answers nothing while verification is off.
  3. Changed: the claim rule is "held where a provider is registered and where its domain list is edited". Right against main: the before-hook asks findSsoDomainClaim on /sso/register and on a domain edit on /sso/update-provider, and the admin form doors re-dispatch through /sso/register.
  4. Added: a dated note under the paragraph naming the proven-domain account rule as this repository's own, and the matching exception in "What this record does NOT settle". Right; N4 and N5 are on its wording.
  5. Unchanged: both anchors (the first is still at line 265), and every mechanism sentence judged in items 2 to 8 of record 6840140663. No mechanism sentence changed meaning.

② Semver level

Unchanged. The skip-changeset label and no changeset; one file under docs/adr/**; no package releases; no Clause-②: line and none owed. Check Changeset skipped on this head, and PR-body guards concluded success. Right.

③ Boundary flags

Check-runs on this head: 48, all completed; 24 success, 22 skipped, 2 failure. The failures are the same two as on 4b57a83691: Lint · docs & code guards (52 gates: 51 passed, 1 red) and its roll-up Lint & Repo Gates. The one red gate is "ADR symbol anchors resolve", with the same single finding: [unresolved-path] at line 265 for the first anchor. None is pending.

④ Design judgment

The delta on record 6840140663.

  • S2: resolved. The note says whose rule this is (this repository's), that it is not a clause of cloud ADR-0024, and that the cloud record's text is not amended. It puts the rule's authority in a ruling and not in the file, which keeps the file's Status true (nothing is decided in it). "This file adds no clause" now names its one exception. A reader holding only the tree no longer reads the rule as cloud's, or as a mirror defect to correct. The Decision preamble's "each clause below" and Provenance's "this file is the bug" are answered at the clause by the note; no further edit is asked.
  • N1: resolved. The condition is bound to the rule sentence, and "off by default" has its subject back.
  • N2: resolved. The claim rule carries its bound, and the bound matches the code.

Does the new text introduce anything wrong? No. Two wordings reach a little further than the file supports; neither changes the rule or who decides it.

  • nit N4. "The one clause this file records that the cloud record did not decide" is true only in the file's own usage. The two Notes under D4 also record things made in this repository, each under a ruling of record, and what the cloud record did not decide is known here only at the granularity of its decision list (Provenance point 2). "The one clause here that rests on no ruling of record" would be exact. An accepted deviation.
  • nit N5. The note names no record: neither fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669, which makes the rule, nor where the ruling is asked. The two Notes under D4 name theirs. It will land reading "pending". A follow-up: when the ruling is given, the note takes its record id, by a one-line governed edit or by the maintainer's amendment at merge.

Line 261 runs to 113 columns, the file's only line over 110. Cosmetic; no gate on this head flagged it.

No must-fix and no should-fix. Nits alone end the round.

Implemented-by: claude/project-thread-ab4lnt
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

VERDICT: PASS

…ccount rule and the PR that implements it

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1fyAgM5kyVcE6pbx2
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 54b1620 · 2026-11-13T01:02Z

Delta review of N4, N5 and the ruling now cited in the D6 note (record 6840500717), same reviewer.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 54b1620a026f6b23c3358f15ab26856a156c29d2
Local-runs: none
Scope: docs/adr/0135-identity-and-access-architecture.md + none

This is the delta on record 6840500717 (head 0815aea4b7: N4, N5). Read: the diff between 0815aea4b7 and this head (one commit, one file, 5 lines added, 3 removed, all inside the D6 note), this PR's refreshed body and its comments, the 48 check-runs on this head, and, for the ruling the note now cites, #32669's comments. Read and not reviewed: #32669 at its current head c4c70109fe, where the rule module is the blob this review read at ec3a30773b (record 6840981958 there). Where this record is brief, records 6840140663 and 6840500717 stand.

① Derived judgments

  1. No accept set and no public surface moves: the delta is 5 lines of one note in a governed record. Right.
  2. Changed: "the one clause this file records that the cloud record did not decide" becomes "like the repository-side facts the D4 notes record". Right: the two Notes under D4 do record things made in this repository, and the note no longer claims to be alone.
  3. Changed: "the ruling is pending" becomes a ruling given: approved by the maintainer on 2026-11-13, with no carve-out, and with a domain entry counting only when written as a bare host name. Right against the record of it on GitHub, the seat's ACCEPT on fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669 (comment 6840983842), which dates the ruling 2026-11-13 00:52Z and quotes the option chosen. Not checkable at its source: ③.
  4. Added: "an entry written as a URL or with a port proves nothing". Right against fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669: the rule counts an entry only when it is already a bare host in its one spelling, one trailing root dot allowed. The paragraph's own clause ("counts only for the host the provider proved") was neutral between the two ways of reading an entry; the note now fixes the one the code holds and the ruling chose.
  5. Added: "fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669 implements it". Right, and true of main by the time this text can be there: the first anchor keeps this PR behind fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669.
  6. Unchanged: the paragraph, both anchors (the first still at line 265), the exception in "What this record does NOT settle", and "That ruling, not this file, is its authority", which keeps the file's Status true.

② Semver level

Unchanged. The skip-changeset label and no changeset; one file under docs/adr/**; no package releases; no Clause-②: line and none owed. Check Changeset skipped on this head, and PR-body guards concluded success. Right.

③ Boundary flags

Check-runs on this head: 48, all completed; 24 success, 22 skipped, 2 failure. None is pending. The failures are the same two as on the two heads before: Lint · docs & code guards (52 gates: 51 passed, 1 red) and its roll-up Lint & Repo Gates. The one red gate is "ADR symbol anchors resolve", with the same single finding: [unresolved-path] at line 265 for the first anchor.

④ Design judgment

The delta on record 6840500717.

Does the new text introduce anything wrong? No. Each new clause was read against #32669's rule module and against the relay of the ruling, and each agrees with both. The note states the ruling's content and adds no mechanism the paragraph's anchor does not already reach: the entry rule lives in the anchored module, sso-account-at-proven-domain.ts.

  • nit N6. The note names the ruling's place by description ("the security lane's decision card"), which a reader holding only the repository cannot open. The two Notes under D4 cite a record id. The record exists and is one step away, through fix(plugin-auth)!: with SSO domain verification on, a provider creates an account only for an email address at a domain it has proven #32669: comment 6840983842 there. Citing that id in the note would close the gap. An accepted deviation, or one clause in a later governed edit.
  • nit N7. The body's "先看" line still calls the paragraph the only change; the diff also holds the note and one line of "What this record does NOT settle". A body edit; no head moves.

No must-fix and no should-fix. Nits alone end the round.

Implemented-by: claude/project-thread-ab4lnt
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Regen-provenance: 6841052407 · 54b1620 → 970b381 · relay pr_update_branch (merge of main, 45 behind; main touched no file of this PR, so the net diff against main is identical before and after) → (empty)

The merge brings in #32669, which the lint anchor in this ADR was waiting on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

席位核对 — ACCEPT · security#1 · head 970b3810a66dbab784c8863084796da2cdedc0f6

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 13, 2026 04:55
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit f2ba3a1 Nov 13, 2026
54 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-ab4lnt branch November 13, 2026 05:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants