Skip to content

fix(plugin-auth)!: an SSO provider's domain list takes bare host names only, and every rule reads each entry the same way - #33267

Queued
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/area-A1-32960
Queued

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/area-A1-32960

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #32960
Fixes #32975

Clause-②: yes (narrowing) — narrowed: POST /sso/register, POST /sso/update-provider and the bridges that re-dispatch to them answer 400 VALIDATION_FAILED on domain (fields[0].code: 'invalid_value', naming the entry) for a domain value holding an entry that is not a bare host name, where they stored it; SCIM adoption of an existing SSO account no longer reads an entry or an address that only folds into a host (full-width letters) as a domain. Widened: SCIM adoption reads an entry and an address in their one spelling, so a root dot and the Unicode / punycode forms of an internationalised name name the same domain, as the account rule already read them. New module exports ssoDomainEntryHost, ssoDomainEntryHosts, ssoDomainEntryNotBare (sso-account-at-proven-domain.ts).
Metadata-protocol: none
Skills-docs: none — no skill under skills/** teaches the SSO provider domain list; content/docs/permissions/sso.mdx gains the rule and the correction in this change.

维护者速读

  • 改了什么:SSO 提供方的「域名列表」只收裸主机名(如 acme.com)。注册或修改时写成网址、带端口或路径(https://acme.com、acme.com:443)会被 400 拒绝并点名那一条,什么都不存。读这个列表的规则(新建账号的已证明域名规则、SCIM 接管已有账号)现在用同一个解析,同一条目在各规则下指同一个域名。
  • 为什么改: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 之后,网址形状的条目会被存下、通过 DNS 验证,却在登录时什么都证明不了;SCIM 接管还用着另一套更松的解析。
  • 风险与回滚:只拒绝新写入的非裸条目;已存的不改写。回滚即还原本 PR。
  • 先看:sso-account-at-proven-domain.ts(ssoDomainEntryHost / ssoDomainEntryNotBare),scim-sso-adoption.ts(改用共享解析),auth-manager.ts assertSsoProviderDomainFree(拒绝的接线)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/sso-account-at-proven-domain.test.ts src/sso-provider-lifecycle.test.ts。
  • 需要你做的:无,评审通过后区域线程落地。

What changes

  • One parse (sso-account-at-proven-domain.ts): ssoDomainEntryHost(entry) (the account rule's entry reading, now exported and named for what it is) and ssoDomainEntryHosts(domain) (the list). The account rule reads through them as before; scim-sso-adoption.ts drops its own split-trim-lowercase parse and reads the provider list through ssoDomainEntryHosts and the address through ssoAddressDomain. So an entry the parse does not accept (a URL, a port, a path) names no domain for SCIM either (fix(plugin-auth): every rule that reads an SSO provider's domain list reads each entry the same way #32975).
  • Register and edit refuse a non-bare entry (feat(plugin-auth): an SSO provider's domain list takes bare hostnames only, refused at register and edit #32960): ssoDomainEntryNotBare(domain) names the first entry the shared parse does not accept; assertSsoProviderDomainFree (the before-hook on /sso/register and a domain edit on /sso/update-provider, which the register bridges reach) throws it as 400 VALIDATION_FAILED on domain, after the length refusal and before the claim rule. Blank entries are skipped, as every parse of the list skips them. Nothing is saved.
  • The claim rule (sso-provider-lifecycle.ts providerDomains, one provider per domain) is unchanged. With the refusal in front, every entry a request can now store reads the same under it as under the shared parse (pinned for every spelling in the table). A stored entry of ANOTHER provider saved before this rule (say https://acme.com) still blocks a collision on acme.com: the claim rule mirrors the vendor's address routing, which still reads it, so reading it more leniently there only ever refuses more.
  • Existing rows are not rewritten; an edit that writes no domain is not judged. content/docs/permissions/sso.mdx says how to correct a stored entry (rewrite it as the bare host, then verify again).

Why

#32960: with domain verification on, #32669 counts an entry as proven only when it is a bare host; a URL- or port-shaped entry was stored, passed the vendor's DNS check, and then admitted no one. #32975: three rules read the same list three ways; one stored entry could name a domain under one and not another.

Verification

At head a10e98535f (branch point origin/main e540baf8; no file this PR touches changed on main since):

  • sso-account-at-proven-domain.test.ts (+7): the account rule and the SCIM rule judged over 14 entry spellings × 5 addresses agree on every pair; non-bare entries name nothing for SCIM; SCIM reads a root dot and punycode as the same domain; CONTROL bare host under all three rules; every entry register accepts reads the same under the claim rule and the shared parse; the refusal names the entry with VALIDATION_FAILED / invalid_value; CONTROL bare names, blanks, empty.
  • sso-provider-lifecycle.test.ts (+4): register (direct door and the SAML bridge) and a domain edit refuse five non-bare shapes with 400 naming the entry, nothing written, the stored domain kept; CONTROL bare names stored at register and edit; a provider stored with a URL entry before the rule is untouched by an edit without a domain and corrected by the documented edit. Two claim cases that used https://… request values now use …. (a root dot) for the same "another spelling" leg, since the shape refusal answers a URL first.
  • Targeted: sso-account-at-proven-domain, sso-provider-lifecycle, scim-sso-adoption: 87 passed.
  • Ablations (fix committed, mutation, restore to HEAD, git diff HEAD empty): SCIM back to its own split-lowercase parse → 2 failed (the agreement table, the one-spelling case); the register refusal unwired in assertSsoProviderDomainFree → 2 failed (both door refusals).
  • @objectstack/plugin-auth full suite: Test Files 269 passed (269), Tests 5125 passed | 10 skipped; typecheck exit 0 (with check:test-typecheck).
  • Gates: the derived families (dispatch-gates --commands) are read in the review request; CI runs the full set.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI).
  • 4. One issue, one branch off an up-to-date main, one pull request (two coupled cards on one function set, one PR, per the area plan).
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is the frame's Chinese summary.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4d1NJYFGVi2FRztyBNo

…, read by one parse in every rule (#32960, #32975)

Claude-Session: https://claude.ai/code/session_016WV4d1NJYFGVi2FRztyBNo
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 14 documentable anchor(s).

16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 199e3624426faa50f413e18657abc44ea20ff3cb.

⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: VALIDATION_FAILED (literal, 46 pages)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 22 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 199e3624426faa50f413e18657abc44ea20ff3cb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a426d6f07172e436350b9ef71eb5865d34d9069a — the merge of head 88e7099e59ac0bcee82457a1bf091251cd4ece82 into base 199e3624426faa50f413e18657abc44ea20ff3cb, 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 a426d6f07172e436350b9ef71eb5865d34d9069a && git checkout a426d6f07172e436350b9ef71eb5865d34d9069a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 199e3624426faa50f413e18657abc44ea20ff3cb 88e7099e59ac0bcee82457a1bf091251cd4ece82 && git checkout -B drift-repro 199e3624426faa50f413e18657abc44ea20ff3cb && git merge --no-ff 88e7099e59ac0bcee82457a1bf091251cd4ece82

node scripts/docs-audit/affected-docs.mjs --json 199e3624426faa50f413e18657abc44ea20ff3cb

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 199e3624426faa50f413e18657abc44ea20ff3cb → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4d1NJYFGVi2FRztyBNo

Request: review · area A1 · Fixes #32960 #32975 · head a10e985 · requested at 2026-11-14T15:19Z

Local at this head: plugin-auth full suite 269 files / 5125 passed; typecheck exit 0; ablations in the body; derived gates running; CI running.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head a10e985 · 2026-11-14T15:21:12Z

review#3 (area-lease pilot #33059, area A1).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a10e98535fc97cf953c32fc96392640c107a5318
Local-runs: none
Scope: packages/plugins/plugin-auth/src/sso-account-at-proven-domain.ts, packages/plugins/plugin-auth/src/scim-sso-adoption.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/sso-account-at-proven-domain.test.ts, packages/plugins/plugin-auth/src/sso-provider-lifecycle.test.ts + packages/plugins/plugin-auth/src/sso-provider-lifecycle.ts, packages/plugins/plugin-auth/src/auth-manager-before-hook.ts, packages/plugins/plugin-auth/src/audience-posture.ts, packages/platform-objects/src/identity/sys-sso-provider.object.ts (read only)

① Derived judgments

  • One parse (fix(plugin-auth): every rule that reads an SSO provider's domain list reads each entry the same way #32975). ssoDomainEntryHost is the former provenEntryHost, exported unchanged in body; ssoProviderProvenDomains now delegates to ssoDomainEntryHosts with identical semantics (skip non-accepted entries, dedupe). scim-sso-adoption.ts drops its own split/lowercase parse and its emailDomain and reads both sides through the shared parse (ssoAddressDomain, ssoDomainEntryHosts), so SCIM and the account rule agree on every entry/address pair (14×5 agreement table pinned). Directions: narrowed (an entry or address that only folds into a host — full-width letters, URL/port/path shapes — no longer vouches for SCIM adoption) and widened (root dot, Unicode/punycode read as one domain, as the account rule already did). Both are declared. The null vs undefined switch at the vouching line is consistent with ssoAddressDomain's return.
  • Register/edit refusal (feat(plugin-auth): an SSO provider's domain list takes bare hostnames only, refused at register and edit #32960). ssoDomainEntryNotBare runs in assertSsoProviderDomainFree after ssoDomainTooLong and before the claim rule, only on /sso/register and /sso/update-provider with a non-blank domain from a caller with a session; blanks skipped like every list parse. The bridges (/admin/sso/register, register-saml, /admin/sso/update-provider) re-dispatch through those paths, pinned for the direct door and the SAML bridge. The generic data layer for sys_sso_provider is read-only (enable.apiMethods), so no other write path stores domain. The console's edit actions (edit_oidc_provider, edit_saml_provider, toggles) send no domain, so an existing provider holding a URL entry stays editable for its other fields; nothing pre-existing is newly blocked from the console.
  • Claim rule (providerDomains, WHATWG host parse) untouched; with the shape refusal in front, every newly storable entry reads the same under both (pinned). A legacy URL entry of another provider still blocks a collision: refuses more, never less. Red line held.
  • nit N1: the docs and changeset say "edit the provider's domain to the bare host names", but no console action edits domain; the only door is POST /api/v1/auth/sso/update-provider (or the admin bridge) with domain. Name the door in both so an operator can act.
  • nit N2 (follow-up, not here): a provider saved before this rule with a URL entry now silently "names no domain" for SCIM adoption too. A one-time boot or Setup warning naming such providers would make the narrowing visible; a p3 loose-ends card at most.

② Semver level

@objectstack/plugin-auth: patch with fix(plugin-auth)! and Clause-②: yes (narrowing) (with its widened arm stated) matches the repo's convention for a runtime refusal at an auth door; no schema key, column or published type moves. New exports are module-level in sso-account-at-proven-domain.ts (not a published subpath change). adr-0087: not-required (no-migration-prescription) is right: stored rows are not rewritten. Metadata-protocol: none holds. Skills-docs: none — REASON acceptable.

③ Boundary flags

  • Security boundary: SSO account creation and SCIM adoption; both moves fail closed except the declared one-spelling widening, which only matches what the account rule already accepted.
  • Author login objectstack-fleet[bot] (relay); implemented by area A1 session_016WV4d1NJYFGVi2FRztyBNo.
  • CI at this head: 29 green, 8 in progress, none red when read (15:22Z). The ACCEPT must re-read checks.

④ Design judgment

Refusing the shape at the write door is the right place: an entry that passes the vendor's DNS check and then proves nothing at sign-in is a silent misconfiguration, and the 400 names the entry and the accepted form. Sharing one parse across the account rule and SCIM removes the three-readings divergence the card describes. Not rewriting stored rows is the conservative choice and is stated. Holds against the cited code and both cards' scenarios.

Implemented-by: claude/area-A1-32960
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

Nits only (N1 text, N2 optional follow-up); no delta owed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4d1NJYFGVi2FRztyBNo

ACCEPT · area A1 · Fixes #32960 #32975 · head 88e7099 · 2026-11-14T15:51Z

  • Reviewer: review#3 (session_016WV4d1PGptBW3QRQNmy5e6), CONTRACT_REVIEW_TIER PASS at a10e985 (record 6879639370).
  • Regen-provenance: prose-only move a10e985 → 88e7099 (N1: content/docs/permissions/sso.mdx + changeset name POST /api/v1/auth/sso/update-provider); review#3 confirmed the PASS carries (record 6879639370).
  • Governed: check-governed-merges.mjs --pr 33267 → NOT governed, 334 changed lines (≤ 3000).
  • Checks at 88e7099: 53 check runs completed, all success/skipped/neutral, none red. Mergeable: clean; main has not changed a file this PR touches.
  • Local: plugin-auth full suite green, typecheck exit 0; derived gates 105: 102 exit 0, 3 NOT MEASURED locally (exit 3, environmental: teardown-shape self-test needs an unshallow clone; skill-examples and dual-build-cjs-loads need a full build), CI ran them green.
  • Follow-up: review N2 (warn about stored URL-shaped domain entries) goes in one p3 loose-ends card.
  • Landing: ready + auto-merge (maintainer 2026-11-14 15:21Z: area PRs that pass review are readied and auto-merged by the area).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 15:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
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 size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants