Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env by claude[bot] · Pull Request #14066 · objectstack-ai/objectstack · GitHub
Skip to content

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env - #14066

Merged
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso
Sep 1, 2026
Merged

feat(spec,plugin-auth): declare plugins.scim/sso/ssoDomainVerification, explicit config wins over env#14066
os-support-ai merged 4 commits into
mainfrom
claude/issue-13439-auth-config-declared-scim-sso

Conversation

@claude

@claudeclaudeBot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes#13439

Clause-②: yes — path limb packages/spec/src/** (auth-config.zod.ts) + content limb (three new declared keys widen the accept set; documented precedence change). Dispatched at CONTRACT_REVIEW_TIER; needs:contract-review hung on this PR and the card at creation.

Implements the maintainer ruling of 2026-08-31 (card comment 5479233268), quoted there verbatim; decision A of the card, admin amplification split out.

What changed

1. Declaration (packages/spec/src/system/auth-config.zod.ts).AuthPluginConfigSchema now declares scim, sso and ssoDomainVerification as tri-state z.boolean().optional(), following the dynamicClientRegistration template, with docstrings spelling out the unset semantics. Previously the keys were read through an as any cast while no schema declared them — a key an author could write, that typechecked only because of the cast, that no publish-time check would ever reject or confirm.

2. Precedence flip (packages/plugins/plugin-auth/src/auth-manager.ts). For these three keys an EXPLICIT config value now wins over the env var; the env var decides only where the config leaves the key unset (absent env means off). The flipped read points, re-derived on current main rather than taken from the card (line numbers at merge time):

buildPluginList scimEffective = pluginConfig.scim ?? scimFromEnv ?? false
buildPluginList sso = pluginConfig.sso ?? ssoFromEnv ?? false
buildPluginList ssoDomainVerification = pluginConfig.ssoDomainVerification ?? ssoDomainVerifyFromEnv ?? false
getPublicConfig admin default = pluginConfig.admin ?? (pluginConfig.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false)
isSsoWired = this.config.plugins?.sso ?? ssoFromEnv ?? false
isSsoDomainVerificationEnabled = this.config.plugins?.ssoDomainVerification ?? fromEnv ?? false

The as any casts on these three keys are gone — the declaration is what makes them typecheck. The stale docstring on isSsoWired (which documented the old env-wins order) is updated, and a comment block above the chains documents the new precedence and why it deliberately differs from the env-wins order the OIDC / DCR / 2FA / HIBP keys keep.

Named consequence — the ADR-0071 admin coupling. The coupling SHAPE is untouched: admin: pluginConfig.admin ?? scimEffective still forces the better-auth admin plugin on when SCIM is effective and admin is unset (ADR-0071's recorded behavior: SCIM active-false to ban runs through the admin plugin). What flows through it is the flipped scim resolution — in both the plugin list and the /auth/config features block's inline recomputation — so an explicit plugins.scim: false now also declines the admin plugin that SCIM would have dragged in. I measured no conflict between the ruled flip and ADR-0071: the ADR pins the coupling (effective SCIM forces admin), not the resolution order of scim itself, and the coupling holds identically after the flip. The coupling itself is out of scope here per the ruling — #13816 tracks it and remains open; nothing in this PR pins its behavior.

Known risk, named (per the ruling): a deployment that writes BOTH an explicit value and the env var and depends on the env winning will flip. The only known explicit writer is the cloud control plane, which requires the new order — its plan-derived plugins.scim becomes authoritative, cloud#1265's refuse-to-build workaround can retire, and cloud#1451 unblocks.

Scope discipline: behavior changes only where a host explicitly sets one of the three keys. Deployments that leave them unset see zero change — the operator per-environment env override is preserved for that case, pinned by tests. The dogfood suites that set OS_SCIM_ENABLED with no explicit config key sit entirely in that unchanged quadrant (their own comments note bootStack exposes no auth-plugin override).

Tests

  • New pins (auth-manager.test.ts), full matrix per the acceptance criteria: explicit false + env truthy stays false (the cloud case, for scim via plugin registration AND the features.admin mirror, for sso via features.sso and isSsoWired, for ssoDomainVerification via isSsoDomainVerificationEnabled); explicit true + env falsy stays true; unset + env set follows env; unset + env absent stays off; explicit false beats the EMPTY-STRING env value (a present value under readBooleanEnv).
  • The pre-existing pin of the OLD order ("env disabled even when plugins.sso true") is rewritten to pin the new order; the unset-config env tests are retitled to say what they now prove.
  • Reverse verification, committed-state, direction observed red: reverting the isSsoWired chain to env-first turned exactly the 9 explicit-config-wins pins red (all others untouched); restore proven by empty git diff against HEAD plus blob-hash identity, then re-run green. Mutation was proven on disk by anchor-string counts before each leg.
  • Reverse type verification: a bogus key read on the config went red with TS2339 whose reported type listed the three new keys — the compiler is reading the rebuilt spec declarations, not a cache.
  • Suites on the final tree (67ab118, after merging origin/main): plugin-auth 87 files / 1796 tests green; spec 447 files / 11991 tests green; both package typechecks green; spec check:generated — all 15 artifacts up to date (docs references + authorable-surface regenerated for the three keys).
  • Gate union at 67ab118: full derived list green, including check:type-check-debt re-measure ("28 ledger entries re-measured, none above its recorded number" — plugin-auth's TEST_DEBT entry did not move; a scoped probe reproduced its 94-total / 43-in-auth-manager.test.ts split exactly), check:dual-build-cjs-loads and spec check:skill-examples after a full workspace build, check:adr-0087-registration ("no declared-breaking changeset"), check:nul-bytes. CI-context gates (shard-attestation, test-completeness, docs drift comment) are CI-owned runs.

Changeset: minor for @objectstack/spec and @objectstack/plugin-auth, behavior change and risk stated, ADR-0087 disposition marker included (not-required, no migration prescription — nothing removed or renamed).

Generated by Claude Code


Generated by Claude Code

…n, explicit config wins over env
Part of #13439 — WIP before artifact regeneration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
…soDomainVerification; regen spec docs + authorable surface
Part of #13439.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mciyv38maJ6HYVMiaM26T1
@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation protocol:system labels Sep 1, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol, a top-level class), OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol, a top-level class))
  • content/docs/permissions/sso.mdx(via getPublicConfig (symbol, a method of class AuthManager))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via OS_SCIM_ENABLED (literal, a string literal in getPublicConfig))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/system.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 132 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4packageMentionDocs.

Which tree this was computed on

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

⚠️ 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 8b04c75d7d4a866789e1dbe13ce5a897d22797e4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-support-aios-support-ai added documentation Improvements or additions to documentation tests tooling protocol:system and removed documentation Improvements or additions to documentation tests tooling needs:contract-review protocol:system labels Sep 1, 2026 — with Claude
@os-support-ai
os-support-ai marked this pull request as ready for review September 1, 2026 02:46
@os-support-ai
os-support-ai added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 8dc22d6Sep 1, 2026
65 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-13439-auth-config-declared-scim-sso branch September 1, 2026 03:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:systemsize/mteststooling

Projects

None yet

2 participants

@os-support-ai@claude