Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .changeset/14361-adr-0071-identity-citations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
'@objectstack/plugin-auth': patch
'@objectstack/platform-objects': patch
'@objectstack/spec': patch
---

docs(identity): re-point the SCIM/identity `ADR-0071` citations at the records that mean them (#14361)

From this repository's point of view `ADR-0071` named two unrelated decisions,
and only one of them had a record here. `docs/adr/0071-dataset-semantic-layer-depth.md`
is *Dataset semantic-layer depth — multi-hop joins*. The identity and SCIM
citations mean something else entirely: the enterprise-identity decision taken in
`objectstack-ai/cloud`, whose open mechanism half has been mirrored into this
repo since 2026-09-07 as
[ADR-0134](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0134-env-side-scim-provisioning.md).
So a reader following one of those citations landed on a real page about the
wrong subject — worse than a dangling id, because a plausible-looking record
invites belief rather than a second question.

44 identity-meaning citations now name the record that holds the decision they
describe. 43 of them read `ADR-0134` (the open mechanism half: effective SCIM
forces the better-auth `admin` plugin on, `active:false` lands as a ban plus
session revocation, the SCIM 2.0 Service Provider mounts in the environment, and
the seven stable `sys_scim_*` models). One reads `cloud ADR-0071` — the
"paid Identity lifecycle" note in `auth-manager.ts`, which names the commercial
half that deliberately stays in the cloud record.

What actually reaches a consumer of these packages:

- `@objectstack/plugin-auth` — the **operator-facing construction-time refusal**
raised when SCIM is effective beside an explicit `plugins.admin: false` now
cites ADR-0134 instead of ADR-0071. The condition that triggers the refusal,
its wording otherwise, and the two documented ways out are unchanged; only the
ADR number in the sentence moves. ⚠️ A deployment that greps that message for
the literal `ADR-0071` should grep for `ADR-0134`.
- `@objectstack/spec` — the `admin` flag's `.describe()` text (shipped both as
`src/system/auth-config.zod.ts` and in the generated `json-schema/` bundle),
and therefore the generated `content/docs/references/system/auth-config.mdx`
reference page app authors read.
- `@objectstack/platform-objects` — the `protection.reason` strings on the eight
`sys_scim_*` identity objects and on `sys_user`.

No behaviour moves. No schema accepts or refuses anything it did not accept or
refuse before, no security or permission semantics are touched, and no ADR
record is written or edited. Bare `ADR-0071` still resolves exactly as it did:
the 22 dataset-meaning citations are byte-identical to `main` and
`check:adr-anchors` reports the same 35477 resolving citations before and after.
Historical archives — the six package CHANGELOGs — are deliberately untouched.
4 changes: 2 additions & 2 deletions content/docs/references/system/auth-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Advanced / low-level Better-Auth options
| **oidcProvider** | `boolean` | optional (default: `false`) | Enable the OpenID Connect provider plugin (acts as an OIDC IdP) |
| **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) |
| **deviceAuthorization** | `boolean` | optional (default: `false`) | Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) |
| **admin** | `boolean` | optional | Enable platform admin operations (ban/unban, set-password, impersonate, set-role). Unset: forced on by effective SCIM (ADR-0071), otherwise off. An explicit false beside effective SCIM is a conflict refused at construction time. |
| **admin** | `boolean` | optional | Enable platform admin operations (ban/unban, set-password, impersonate, set-role). Unset: forced on by effective SCIM (ADR-0134), otherwise off. An explicit false beside effective SCIM is a conflict refused at construction time. |
| **phoneNumber** | `boolean` | optional (default: `false`) | Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) |
| **scim** | `boolean` | optional | Enable the SCIM 2.0 provisioning surface. Unset: OS_SCIM_ENABLED decides (absent = off); an explicit value wins over the env var. Effective SCIM forces the admin plugin on when admin is unset; an explicit admin: false beside effective SCIM is refused at construction time. |
| **sso** | `boolean` | optional | Enable enterprise SSO (domain-routed OIDC/SAML sign-in). Unset: OS_SSO_ENABLED decides (absent = off); an explicit value wins over the env var. |
Expand Down Expand Up @@ -212,7 +212,7 @@ OIDC / Generic OAuth2 provider configuration for enterprise SSO
| **oidcProvider** | `boolean` | optional (default: `false`) | Enable the OpenID Connect provider plugin (acts as an OIDC IdP) |
| **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) |
| **deviceAuthorization** | `boolean` | optional (default: `false`) | Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) |
| **admin** | `boolean` | optional | Enable platform admin operations (ban/unban, set-password, impersonate, set-role). Unset: forced on by effective SCIM (ADR-0071), otherwise off. An explicit false beside effective SCIM is a conflict refused at construction time. |
| **admin** | `boolean` | optional | Enable platform admin operations (ban/unban, set-password, impersonate, set-role). Unset: forced on by effective SCIM (ADR-0134), otherwise off. An explicit false beside effective SCIM is a conflict refused at construction time. |
| **phoneNumber** | `boolean` | optional (default: `false`) | Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) |
| **scim** | `boolean` | optional | Enable the SCIM 2.0 provisioning surface. Unset: OS_SCIM_ENABLED decides (absent = off); an explicit value wins over the env var. Effective SCIM forces the admin plugin on when admin is unset; an explicit admin: false beside effective SCIM is refused at construction time. |
| **sso** | `boolean` | optional | Enable enterprise SSO (domain-routed OIDC/SAML sign-in). Unset: OS_SSO_ENABLED decides (absent = off); an explicit value wins over the env var. |
Expand Down
2 changes: 1 addition & 1 deletion docs/qa/platform-checklist/areas/identity-auth.json
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@
"packages/plugins/plugin-auth/src/auth-route-ledger.ts#BETTER_AUTH_MOUNTED_SURFACE (BETTER_AUTH_MOUNTED_SURFACE admin/* rows: list-users, create-user, set-role, remove-user, revoke-user-session(s))",
"packages/plugins/plugin-auth/src/admin-user-endpoints.ts#resolvePassword (create-user resolvePassword: explicit password wins over generatePassword — #3031/#3033; leaves sys_user + credential sys_account)",
"packages/plugins/plugin-security/src/security-plugin.ts#__referentialFieldClear (§A5 #3023 EXEMPTION: __referentialFieldClear owner_id-null cascade rides a server-derived context, the owner-anchor guard must not veto it) + security-plugin.test.ts '[#3023] … engine referential FK clear … is exempt'",
"packages/spec/src/kernel/public-auth-features.ts#sys_user (admin flag gates the sys_user lifecycle actions; SCIM forces it on — ADR-0071)",
"packages/spec/src/kernel/public-auth-features.ts#sys_user (admin flag gates the sys_user lifecycle actions; SCIM forces it on — ADR-0134)",
"packages/qa/dogfood/test/admin-identity-audit-trail.dogfood.test.ts",
"packages/plugins/plugin-auth/src/session-of-record.test.ts#body (better-auth answers /get-session with HTTP 200 + a JSON null body when the session is gone — NOT 401; a status-only assertion would pass against a fully revoked session)"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const SysScimConnectionBinding = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Lifecycle state for a SCIM provisioning connection, including its decommission state machine',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';

/**
* sys_scim_connection_credential — ObjectStack-owned bearer credentials for
* SCIM provisioning connections (#3653, ADR-0071).
* SCIM provisioning connections (#3653, ADR-0134).
*
* Stable `@better-auth/scim` stores NO credential of its own: the rc.1
* `/scim/generate-token` endpoint and `scimProvider.scim_token` column are
Expand Down Expand Up @@ -47,7 +47,7 @@ export const SysScimConnectionCredential = ObjectSchema.create({
// ADR-0010 §3.7 — platform-managed identity table; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'ObjectStack-owned SCIM credential store (#3653) — see ADR-0071.',
reason: 'ObjectStack-owned SCIM credential store (#3653) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Bearer credentials (one-way digests) that authenticate SCIM provisioning connections',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const SysScimGroupMember = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'SCIM group membership rows pushed by the IdP (group ↔ provisioned user)',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const SysScimGroup = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'IdP-pushed SCIM 2.0 groups, scoped per provisioning connection',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const SysScimIdentityTombstone = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Tombstones of IdP-deleted SCIM identities, kept so a re-provision is recognized',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const SysScimProjectionGrant = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Role/entitlement grants projected onto platform users by SCIM provisioning',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const SysScimSubject = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Per-user SCIM provisioning link — one row per user any SCIM connection provisions',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const SysScimUser = ObjectSchema.create({
// ADR-0010 §3.7 — managed by better-auth; tenants may not edit schema.
protection: {
lock: 'full',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0071.',
reason: 'Identity table managed by better-auth (@better-auth/scim) — see ADR-0134.',
docsUrl: 'https://objectstack.ai/docs/references/shared/protection',
},
description: 'Per-connection projection of an IdP-provisioned user (SCIM 2.0 /Users)',
Expand Down
2 changes: 1 addition & 1 deletion packages/platform-objects/src/identity/sys-user.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const SysUser = ObjectSchema.create({
// `requiresFeature: 'admin'` (#2874) — when the plugin is off the UI
// hides them instead of rendering buttons that 404. SCIM deployments
// keep them: SCIM forces the admin plugin (and `features.admin`) on
// (ADR-0071). UI surfaces them under the row menu AND the
// (ADR-0134). UI surfaces them under the row menu AND the
// record-detail header (`record_header`, overflowing into the ⋯
// "More" menu) so platform admins can manage an account from either
// the Users list or an open user record — without dropping to SQL or
Expand Down
18 changes: 9 additions & 9 deletions packages/plugins/plugin-auth/src/auth-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ describe('AuthManager', () => {
});

// @better-auth/scim mounts the SCIM 2.0 Service Provider so an external IdP
// can auto-provision/deprovision this env's users (ADR-0071). It is opt-in
// can auto-provision/deprovision this env's users (ADR-0134). It is opt-in
// via `plugins.scim` (explicit value wins, #13439) or OS_SCIM_ENABLED
// (decides where the config leaves it unset), and effective SCIM FORCES
// the admin plugin on (active:false → ban runs through admin).
Expand Down Expand Up @@ -508,7 +508,7 @@ describe('AuthManager', () => {
// config leaves the key unset. This is the cloud control plane's case:
// its plan-derived `plugins.scim: false` must be authoritative even in a
// deployment env that carries an ambient OS_SCIM_ENABLED (cloud#1265).
// The forced-admin coupling (ADR-0071) follows the EFFECTIVE scim value,
// The forced-admin coupling (ADR-0134) follows the EFFECTIVE scim value,
// so declining scim also declines the admin plugin it would have dragged
// in (unless `admin` is set explicitly).
it('should NOT register the scim plugin (nor force admin on) when plugins.scim=false despite OS_SCIM_ENABLED', async () => {
Expand Down Expand Up @@ -559,7 +559,7 @@ describe('AuthManager', () => {
await manager.getAuthInstance();
const ids = capturedConfig.plugins.map((p: any) => p.id);
expect(ids).toContain('scim');
// ADR-0071 — the forced-admin coupling is unchanged: effective SCIM
// ADR-0134 — the forced-admin coupling is unchanged: effective SCIM
// still drags the admin plugin in when `admin` is left unset.
expect(ids).toContain('admin');
expect(manager.getPublicConfig().features.admin).toBe(true);
Expand All @@ -572,19 +572,19 @@ describe('AuthManager', () => {

// #13816 (maintainer ruling 2026-09-01) — `admin` is tri-state, and the
// one incoherent corner is REFUSED at construction: effective SCIM with an
// explicit `plugins.admin: false` contradicts ADR-0071 (SCIM's
// explicit `plugins.admin: false` contradicts ADR-0134 (SCIM's
// active:false deprovisioning runs through the admin plugin), so the
// manager throws a documented conflict instead of silently honouring the
// decline and mounting SCIM with a broken deprovisioning path.
it('REFUSES construction when plugins.scim=true and plugins.admin=false (documented ADR-0071 conflict)', () => {
it('REFUSES construction when plugins.scim=true and plugins.admin=false (documented ADR-0134 conflict)', () => {
const prev = process.env.OS_SCIM_ENABLED;
delete process.env.OS_SCIM_ENABLED;
try {
expect(() => new AuthManager({
secret: 'test-secret-at-least-32-chars-long',
baseUrl: 'http://localhost:3000',
plugins: { scim: true, admin: false },
})).toThrow(/plugins\.admin[\s\S]*ADR-0071[\s\S]*plugins\.scim: false/);
})).toThrow(/plugins\.admin[\s\S]*ADR-0134[\s\S]*plugins\.scim: false/);
} finally {
if (prev === undefined) delete process.env.OS_SCIM_ENABLED;
else process.env.OS_SCIM_ENABLED = prev;
Expand All @@ -599,7 +599,7 @@ describe('AuthManager', () => {
secret: 'test-secret-at-least-32-chars-long',
baseUrl: 'http://localhost:3000',
plugins: { admin: false },
})).toThrow(/OS_SCIM_ENABLED[\s\S]*ADR-0071/);
})).toThrow(/OS_SCIM_ENABLED[\s\S]*ADR-0134/);
} finally {
if (prev === undefined) delete process.env.OS_SCIM_ENABLED;
else process.env.OS_SCIM_ENABLED = prev;
Expand Down Expand Up @@ -693,7 +693,7 @@ describe('AuthManager', () => {
plugins: { scim: true },
});
expect(() => manager.applyConfigPatch({ plugins: { admin: false } }))
.toThrow(/ADR-0071/);
.toThrow(/ADR-0134/);
// The refused patch never became current: the advertised admin flag
// still reflects the SCIM-forced coupling.
expect(manager.getPublicConfig().features.admin).toBe(true);
Expand All @@ -717,7 +717,7 @@ describe('AuthManager', () => {
// appearing between construction and the lazy build must not mount
// SCIM with its deprovisioning path silently declined.
process.env.OS_SCIM_ENABLED = 'true';
await expect(manager.getAuthInstance()).rejects.toThrow(/ADR-0071/);
await expect(manager.getAuthInstance()).rejects.toThrow(/ADR-0134/);
} finally {
if (prev === undefined) delete process.env.OS_SCIM_ENABLED;
else process.env.OS_SCIM_ENABLED = prev;
Expand Down
Loading
Loading