Skip to content

docs(adr): ADR-0134 mirrors the open half of cloud ADR-0071 — env-side SCIM, and this platform writes the deactivation - #16476

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-14507-adr-mirror-scim-env-side
Sep 7, 2026
Merged

docs(adr): ADR-0134 mirrors the open half of cloud ADR-0071 — env-side SCIM, and this platform writes the deactivation#16476
os-zhuang merged 1 commit into
mainfrom
claude/issue-14507-adr-mirror-scim-env-side

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #14507

GOVERNED SURFACE — draft PR, human merge. docs/adr/** is on the governed register, so no seat flips this ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14). Verified on this branch, both directions:

probe command exit
positive node scripts/pm/check-governed-merges.mjs --test docs/adr/0134-env-side-scim-provisioning.md 3 — GOVERNED (docs/adr/** x1 — architecture decision records)
negative control node scripts/pm/check-governed-merges.mjs --test packages/plugins/plugin-auth/src/auth-manager.ts 0 — NOT governed

What this writes

One new record, docs/adr/0134-env-side-scim-provisioning.md (312 lines, the only file in the diff). It is a mirror of the open, mechanism half of cloud ADR-0071 (Enterprise Identity — SCIM v1, Status there: Proposed, 2026-06-27), written in this repo's own words under the maintainer's 2026-09-02 ruling on #14496 (option 2 — mirror the open half, do not move files, do not renumber).

The number 0134 was assigned by the PM seat, not chosen as "the next free slot": origin/main tops out at 0131, 0132 is the sibling mirror card #14506, and 0133 is taken by the in-flight PR #16267 (card #14508 — confirmed on this branch by diffing that remote head, which adds docs/adr/0133-org-management-open-basics.md). 0134 is free on origin/main and on every remote head.

Restated, with the cloud record's own numbering kept so the two documents read side by side:

  • D1 — adopt @better-auth/scim as the mechanism; the seven vendor models are bridged to sys_scim_* platform objects, never reimplemented.
  • D2 — the SCIM Service Provider lives in the env, never in the cloud control plane; identical for cloud-managed and self-hosted. Toggled per environment through the tri-state plugins.scim / OS_SCIM_ENABLED; connections are runtime data resolved by an app-owned bearer verifier.
  • D3 — single-organization mode; active:false disables and revokes sessions, DELETE never erases the user (the vendor tombstones the source and the disable arrives through the same path).
  • D4 — V1 provisions the default standing only; group-to-role mapping is a declared, unwired seam on both sides.
  • D7, open half — the SCIM conformance / smoke coverage that runs in this repo's CI test lane, named file by file.

What stays in cloud (cited as cloud ADR-0071, not restated here)

D5 (the open/closed boundary), D6 (the commercial repositioning: paid = governed / supported / auditable SCIM), the real-IdP end-to-end run on cloud's staging, and the Enterprise Identity pillar framing. The record says so explicitly and adds nothing the cloud record did not decide.

The vendor-version fact — re-measured on this branch, not copied from the card

The card carried a reading; per the dispatch this branch measured it independently, on the packages themselves.

@better-auth/scim occurrences of ban in dist/index.mjs identity.reconcileUser hook
1.6.30 (last 1.6.x; 1.6.31 is a registry 404) 14 — incl. resolveSCIMActiveDeactivation, which sets banned: true and banReason to the literal Deactivated via SCIM absent
1.7.0 0 present
1.7.1 0 present
1.7.2 — this repo's pin 0 present

The pin, read from this worktree: packages/plugins/plugin-auth/package.json declares ^1.7.2, pnpm-lock.yaml resolves @better-auth/scim@1.7.2, and the installed package's own package.json reports 1.7.2. In the installed 1.7.2 the reconciliation path calls the optional host hook identity.reconcileUser with the aggregate state and then, when the aggregate is inactive, deletes the user's sessions — and writes no ban.

Ban ownership, stated as the record states it: this platform writes it, not the vendor. auth-manager.ts's reconcileScimUserLifecycle implements the host hook and lands the disable through user-ban-write.ts's applyUserBan / SCIM_DEACTIVATION_BAN_REASON — field for field the write the admin mount makes — and it is judged at the engine by the break-glass last-administrator guard. The admin plugin is still required because it supplies the column and the BANNED_USER sign-in refusal, which is what keeps assertScimAdminCoherence coherent. Session revocation stayed with the vendor. This matches the card, and the wiring is already on main (#14360), so nothing here waited on it.

Code anchors — every one opened and confirmed on this branch

All in symbol-anchor form (path#symbol); pnpm check:adr-symbol-anchors resolves every one of them.

  • packages/plugins/plugin-auth/src/auth-manager.ts — the scim({...}) options block, resolveScimEnabled, assertScimAdminCoherence, reconcileScimUserLifecycle, SCIM_PROTOCOL_PATH_PREFIX, isScimProtocolPath
  • packages/plugins/plugin-auth/src/user-ban-write.tsapplyUserBan, applyUserUnban, SCIM_DEACTIVATION_BAN_REASON, UserBanWriter
  • packages/plugins/plugin-auth/src/last-admin-guard.tsregisterLastAdminGuard; packages/plugins/plugin-auth/src/admin-ban-endpoints.tsrunAdminBanUser / runAdminUnbanUser
  • packages/plugins/plugin-auth/src/scim-connection-service.tsverifyScimBearerToken; packages/plugins/plugin-auth/src/objectql-adapter.tsAUTH_MODEL_TO_PROTOCOL
  • packages/platform-objects/src/identity/sys-scim-*.object.ts — all eight objects, incl. SysScimProjectionGrant (the D4 seam) and SysScimConnectionCredential.organization_id (the D3 seam)
  • packages/spec/src/system/auth-config.zod.ts — the plugins.scim and plugins.admin describe blocks; packages/spec/src/identity/scim.zod.tsSCIM_SCHEMAS

Clause-②: no. packages/spec/** was read as an anchor and is not in the diff; nothing needed changing there.

One reader-safety fact the record states in its Provenance, because it is load-bearing for #14361: docs/adr/0071-* in this repo is Dataset semantic-layer depth, an unrelated record. A bare ADR-0071 in identity code therefore resolves, by this repo's own convention, to the wrong document. Always write cloud ADR-0071 for the SCIM record and ADR-0134 for this one.

Changeset

None — skip-changeset, which is what every recent docs/adr/**-only landing did (245c6a23c, 932acc3df, 3a534facb, e6c99643e each land the ADR alone, with no .changeset/*.md). The judgement is the rule, not the list: this PR publishes nothing from any package.

Gates — the full derived family, exit codes captured before any pipe

Derived mechanically: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at commit 5b987ca64 (change set from the merge base, three-dot). 17 commands, all run, all green.

# command exit
1 node scripts/check-adr-links.mjs 0 — 654 relative link destination(s) under docs/adr/ resolve
2 node scripts/check-adr-links.mjs --self-test 0
3 node scripts/check-adr-symbol-anchors.mjs 0 — 2013 anchors across 136 records resolve … 0 line anchors survive
4 node scripts/check-adr-symbol-anchors.mjs --self-test 0
5 node scripts/check-ci-filter-parity.mjs 0
6 node scripts/check-closing-keyword-parity.mjs 0
7 node scripts/check-closing-keyword-parity.mjs --self-test 0
8 node scripts/check-comment-mask-corpus.mjs 0 — 6250 files, 0 disagree
9 pnpm --filter @objectstack/lint run check:doc-formula-expressions 3 first (PREREQUISITE NOT MET, nothing measured), then 0 after turbo run build --filter=@objectstack/formula --filter=@objectstack/lint
10 pnpm check:adr-anchors 0 (pre-existing allowlisted notes only)
11 pnpm check:cross-package-test-inputs 0
12 pnpm check:doc-authoring 0
13 pnpm check:driver-memory-census 0
14 pnpm check:nul-bytes 0 — 8084 text file(s); no raw ASCII control bytes
15 pnpm check:pm-governed-merges 0
16 pnpm check:refd-timer-probe 0
17 pnpm check:watch-hint-literal 0

Run at head 5b987ca64, which is this PR's final commit. Every exit code was captured with cmd > log 2>&1; EXIT=$? — never through a pipe. The one exit 3 is reported as NOT MEASURED, then as a real green after its declared prerequisite was built; it is not counted as a pass in the first state.

Not claimed as measured: the whole-tree families dispatch-gates declares too wide to place, the three that take a value from the workflow, and the 41 artifact rosters — CI runs the farm.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y


Generated by Claude Code

…e SCIM, with the ban written by this platform

Mirrors the mechanism half of `cloud ADR-0071` (Enterprise Identity — SCIM v1,
2026-06-27) into this repo under the maintainer's 2026-09-02 ruling on #14496:
D1 (adopt @better-auth/scim), D2 (the Service Provider lives in the env),
D3 (single-organization; active:false disables, DELETE never erases), D4 (default
standing only, group->role a declared seam) and the open half of D7 (CI coverage).

D5, D6 and the real-IdP E2E stay in cloud and are cited as `cloud ADR-0071`.

The record states, and this branch re-measured, the vendor-version fact: at the
pinned @better-auth/scim 1.7.2 the substring `ban` occurs zero times in the
package (1.6.30: 14, incl. resolveSCIMActiveDeactivation; removed in 1.7.0), so
the disable is written by this platform's identity.reconcileUser hook and judged
at the engine by the break-glass last-administrator guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation labels Sep 7, 2026
@os-warren os-warren added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed documentation Improvements or additions to documentation size/m labels Sep 7, 2026 — with Claude
os-warren pushed a commit that referenced this pull request Sep 7, 2026
…aimed by an open PR)

`0132` is free on `main` but claimed on the branch of an open PR — #16215 adds
`docs/adr/0132-multi-organization-runtime-is-open-core.md`. Verified here by a
full `git diff --name-only origin/main...<pr-head> -- docs/adr/` over all 27
open PRs, not by titles: 0132 is #16215's, 0133 is #16267's, 0134 is #16476's,
and 0135 is unclaimed.

File renamed with `git mv`, slug unchanged. The record's one self-citation (its
title line) moves with it; the `ADR-0105` and `ADR-0108` references are landed
records and are untouched. No line-number anchors were introduced — the file
still carries none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
@os-zhuang
os-zhuang marked this pull request as ready for review September 7, 2026 06:36
@os-zhuang
os-zhuang requested a review from hotlong as a code owner September 7, 2026 06:36
@os-zhuang
os-zhuang enabled auto-merge September 7, 2026 06:36
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit aefab7b Sep 7, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-14507-adr-mirror-scim-env-side branch September 7, 2026 07:01

Copy link
Copy Markdown
Collaborator Author

PM 独立复核 —— PASS

独立 worktree,checkout 到 head 5b987ca64,⛔ 不采信席位报告,⛔ 未使用共享主 checkout。

检查 读数
三点 diff 1 个文件docs/adr/0134-env-side-scim-provisioning.md
check-governed-merges.mjs --test <该文件> EXIT=3 · GOVERNED ⇒ 保持 draft、人工合并、⛔ 不 arm
check-adr-anchors.mjs EXIT=0
check-adr-symbol-anchors.mjs EXIT=02013 anchors across 136 records resolve … 0 line anchors survive
check-adr-links.mjs EXIT=0654 relative link destination(s) under docs/adr/ resolve
path:NNN 行锚点 0 处
ADR 编号 0134 #16215(0132,已落地)、#16267(0133)、#16478(0135)均不冲突

⭐ 这份镜像最有价值的一段,是它纠正了 cloud 原记录

cloud ADR-0071(2026-06-27)把停用描述为 active:false → 经 admin 插件 ban → 吊销会话。席位在实际的包上逐版本量了(⛔ 不是抄卡):

@better-auth/scim dist/index.mjsban 出现次数 identity.reconcileUser 宿主钩子
1.6.30 14(含 resolveSCIMActiveDeactivation,写 banned: true / banReason: 'Deactivated via SCIM'
1.7.0 / 1.7.1 0
1.7.2(本仓 lockfile 解析到的版本) 0

⇒ 上游在 1.7.0 移除了 ban。今天写这个 ban 的是本平台自己reconcileScimUserLifecycle 实现 vendor 的可选钩子,落到 applyUserBan,且与 admin 面的写入字段对字段相同、共用一个 callable。

⭐ 这是镜像存在的意义的最好例证:它不是把 cloud 记录抄一遍,而是把今天真实的机制写对,并把那句话标了日期——因为 D1 把一个第三方放上了离职停用路径,机制的真相从此是一个版本事实

与本轮组织包搬迁(#16215 / ADR-0132)的关系

PM 实测:本文件不受影响。它的商业半边是 cloud ADR-0071 的 D5 / D6(开闭边界的治理、以及「付费 = 受治理 / 受支持 / 可审计的 SCIM」),与多组织运行时开源无关;文中「单组织 V1、多组织留缝」的表述在 packages/plugins/organizations 进入开源仓之后仍然成立——SCIM V1 的单组织范围是它自己的决定,不是权益边界的推论。

(受影响的是姊妹镜像 ADR-0133 / PR #16267,已更新并复核。)

保持 draft,等人工合并。 PM 未 flip ready、未 arm auto-merge、未 update_pull_request_branch


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants