Repository navigation
SCIM: 停在 @better-auth/scim rc.1,等正式版再整体迁移 —— rc.2 换掉了整套模型 #3653
Description
Restart-when: npm view @better-auth/scim dist-tags shows a stable (non-rc) latest — then open the whole-model migration as its own architecture card (#5024 probe fires this)
重写。 原帖(以及第一次更新)问的是"要不要补齐 SCIM group 的四张平台对象表"。去调研 rc.2 之后,这个问题的前提没有了 —— rc.2 把 SCIM 整个重设计了,rc.1 的那四张表在上游已经不存在。所以本 issue 现在记录的是一个决定和它的理由,不是一个待办。
决定
停在 @better-auth/scim@1.7.0-rc.1,不补 group 表,不升 rc.2。等 SCIM 出正式版之后,作为一次独立的架构迁移一起做。
为什么不补 rc.1 的四张表
照 rc.1 实现,等于照着上游已经废弃的 schema 建四张表:
scimGroupRole/scimGroupRoleGrant在 rc.2 里已经没了;scimGroup/scimGroupMember名字还在,但列完全不同(rc.2 的scimGroup多了connection_id/revision/display_name_key/order_key)。
写出来就是即刻过时的迁移债,而且是带数据的那种。
为什么不升 rc.2
rc.2 不是一次版本升级,是一次架构迁移。三点,按严重程度排:
1. scimProvider 整个消失了
rc.2 里全文 0 处引用。而 sys_scim_provider 是 ObjectStack 唯一有的 SCIM 平台对象 —— 也就是 #3688 刚给它补上 provider_key 的那张表。它在 rc.2 下没有对应的上游模型。
2. 模型换了一套
| 模型 | |
|---|---|
| rc.1(当前) | scimProvider、scimGroup、scimGroupMember、scimGroupRole、scimGroupRoleGrant |
| rc.2 | scimConnectionBinding、scimIdentityTombstone、scimSubject、scimUser、scimProjectionGrant、scimGroup、scimGroupMember |
5 个换 7 个,只有两个名字重合,而这两个的列也不一样。
3. 连接从"运行时数据行"变成了"启动时配置"
rc.2 的 scim() 在构造时强制要求静态声明 connections:
BetterAuthError: The scim plugin requires at least one provisioning connection.
而 auth-manager.ts 现在传的是 scim({ storeSCIMToken: 'hashed' }) —— 没有 connections,rc.2 下直接抛错。
这一条比 schema 变更严重得多:它把 SCIM 连接从「数据库行 + /scim/generate-token 端点 + Setup UI 管理」搬到了「进程启动配置」。token 由谁产生、存在哪、怎么轮换,全都变了,连带影响 Setup UI 和 generate-token 那一整条链路。
现在是什么状态(不是"坏的",是"有边界的")
- SCIM 默认关闭(
OS_SCIM_ENABLED),爆炸半径仅限显式开启的部署。 - provider 创建这条路是通的 —— fix(auth): close the sso/scim parity hole; provision sys_scim_provider.provider_key (#3653) #3688 补上了
provider_key,那是 SCIM 一打开就会撞上的第一个坑。 - group 供给不通:IdP 往
/Groups推送会写四张不存在的表,和 org creation over HTTP 500s when teams are enabled: better-auth 1.7.0-rc.1 default team carries memberCount, sys_team lacks the column/mapping #3624 同一个失败形状。这四个 model 在 rc.1 的插件代码里是无条件使用的,没有开关能关掉它。
所以开启 SCIM 的部署,今天不要让 IdP 推送 group。 这是接受这个决定的代价,写在这里而不是让人踩。
parity gate 怎么守住这段时间
gate(better-auth-schema-parity.test.ts)没有假装覆盖到了。四个无对象的 model 在一个 KNOWN_UNMAPPED_MODELS 精确集合里,并且集合本身被断言:
- 出现新的无对象 model → 构建失败;
- 某个 model 不再在集合里(说明已经 provision 了)→ 也失败,提示把它挪进列检查。
也就是说:真去升 rc.2 的那一刻,gate 会立刻把这七个模型的差异全部报出来,不会有第二个"悄悄长出来的洞"。这正是它该起的作用。
重启这件事的触发条件
@better-auth/scim 发布正式版(非 rc)。到那时一并处理,作为一个独立的迁移任务:
- 按正式版的模型集补齐平台对象(届时以正式版为准,不是 rc.2);
- 决定
sys_scim_provider的去向 —— 迁移还是废弃; - 重做连接配置模型(静态
connectionsvs 现有的generate-token+ Setup UI); auth-manager.ts的scim({...})调用补上必需参数。
一处独立的遗留张力(与版本无关)
sys_scim_provider 上有 { fields: ['provider_id'], unique: true },但 rc.1 上游的唯一性边界是 <organization>:<provider_id> —— 同一个 provider_id 在两个不同组织下,上游认为合法,而这个索引会拒绝。
比库假设的约束更严。#3688 里没有动它:放宽一个已经生效的唯一约束是独立的决定,而且要先确认当初是不是有意为之。记在这里,不随 SCIM 迁移绑定 —— 它今天就可以单独判断。
已经做完的部分(原帖的问题)
原帖最初问的是"parity gate 覆盖不到 sso/scim"。这一点已经修好并合并:两个 plugin 虽然不接受 schema option,但它们自己暴露 .schema,而 adapter 对 bridged model 的列名规则是机械的 camelCase → snake_case。gate 现在读前者、套后者,算出它们真正会写的列。
@better-auth/sso完全干净 ✅sys_scim_provider缺provider_key→ 已补(fix(auth): close the sso/scim parity hole; provision sys_scim_provider.provider_key (#3653) #3688)
Refs #3624, #3647, #3688, ADR-0071。
Generated by Claude Code
Activity
[-]better-auth column-parity gate does not cover @better-auth/sso / @better-auth/scim — sys_sso_provider / sys_scim_provider can still lose a column on upgrade[/-][+]SCIM group provisioning has no platform objects — 4 of @better-auth/scim's 5 models are unprovisioned[/+]os-zhuang commentedon Jul 27, 2026
补充:这不只是"缺四张表",spec 已经在对外宣称 group 供给
顺着 #3688 的 docs-drift 提示查下去,发现缺口比正文写的更靠前一层 ——
packages/spec声明了 SCIM group 的类型,但没有任何运行时消费它们:对
SCIMGroup做全仓搜索,spec 之外零个消费者(排除 dist 和测试)。而这些类型是对外发布的:
content/docs/references/identity/scim.mdx—— 自动生成,已经列出SCIMGroup/SCIMGroupReference/SCIMMemberReference的完整字段表content/docs/getting-started/quick-reference.mdx:183—— 把SCIMUser, SCIMGroup并列为 "SCIM 2.0 provisioning" 的代表类型content/docs/references/identity/scim.mdx:16—— "SCIM 2.0 is an HTTP-based protocol for managing user and group identities"所以现在的状态是:协议层宣称支持 group 供给 → 类型发布给了第三方 → 但没有任何东西持久化 group。这正是 AGENTS.md Prime Directive #10 点名的
declared ≠ enforced形状,和当年"spec 声明 9 种校验规则、写路径只实现 3 种"(#1475)是同一类。这让"建议"里的第 1 条更具体了
原来写的是"决定是否要支持 SCIM group 供给"。现在这个决定有了明确的两个分支,和 #1475 的收尾方式一致 —— 要么实现,要么裁掉:
sys_scim_group/sys_scim_group_member/sys_scim_group_role/sys_scim_group_role_grant),让宣称成真;SCIMGroupSchema一族从 spec 移除(按 changeset 的 breaking 规范给出 FROM → TO 与 tombstone),让文档不再宣称做不到的事。中间状态是最坏的:第三方照着
SCIMGroup的类型写集成,IdP 一推 group 就撞到不存在的表。Generated by Claude Code
fix(auth): close the sso/scim parity hole; provision sys_scim_provide…
[-]SCIM group provisioning has no platform objects — 4 of @better-auth/scim's 5 models are unprovisioned[/-][+]SCIM: 停在 @better-auth/scim rc.1,等正式版再整体迁移 —— rc.2 换掉了整套模型[/+]chore(scripts): watch npm for the stable release that retires a prere…
os-zhuang commentedon Aug 6, 2026
存量裁决轮(维护者 2026-08-06 委托,session_01LeEfA7CFwbJb7JJmXm2KM3):补落
pm:on-hold——本单正文记录的本就是一个已做裁决(停 scim rc.1、不追上游废弃表、不推倒重写 rc.2 架构),parity gate 已用 KNOWN_UNMAPPED_MODELS 精确集合守住这段时间,本次把它落进状态机。重启条件:@better-auth/scim发布正式版(非 rc;#5024 探针触发)→ 作为独立架构迁移立项。Generated by Claude Code
test(plugin-auth): pin credential-at-rest posture for scim_token / cl…
fix(auth): absorb the rc.2 → stable 1.7 vendor drift on the auth surface
40 remaining items
feat(auth,platform-objects,spec): migrate @better-auth/scim to stable…
feat(cli,create-objectstack): retire the @better-auth/scim>better-cal…
fix(scripts): retire check-prerelease-pin-watch's repo-anchored self-…
fix(auth): scope the adapter's native transactions to SCIM protocol r…
os-sales commentedon Aug 27, 2026
os-dev-report
{ "issue": 3653, "status": "done", "branch": "claude/issue-3653-scim-stable-pin-move", "pr": "https://github.com/objectstack-ai/objectstack/pull/12726", "premise_still_valid": true, "summary": "Leg 2-prime landed whole on the measurement base (c87d731f6 + merge of origin/main 15bf9e8): pin resolved to 1.7.1 EXACTLY per the ACCEPT ruling (the base's caret-range resolved 1.7.2 and was tightened), seven stable models provisioned as sys_scim_* platform objects plus the ObjectStack-owned sys_scim_connection_credential (mint+store+verify in scim-connection-service.ts, keyed HMAC-SHA-256 at rest - parity or better vs rc.1's unsalted SHA-256), AUTH_MODEL_TO_PROTOCOL bridged, KNOWN_UNMAPPED_MODELS shrunk to the empty exact set, auth-manager on the stable verifyBearerToken constructor, registry +8 names, epic item 7 retired with ratchets flipped, translations regenerated by script, changeset argued minor. Two things the dispatch could not have known were measured and handled: stable scim REFUSES adapters without a native transaction function, and both unscoped implementations broke measured surfaces (fail-closed 500'd every sign-up on memory engines; unconditional real transactions deadlocked the dogfood showcase boot on single-connection sqlite pools, in CI and reproduced locally) - resolved by scoping real engine transactions to authenticated SCIM requests via an AsyncLocalStorage marker set in the verifier, leaving every core better-auth flow at its historical sequential posture. Two CI reds on the PR were root-caused and fixed by push (prerelease-pin-watch self-test retirement leg; the dogfood deadlock). PM ACCEPT prerequisites honored: parity gate never widened, credential-at-rest test restructured honestly (upstream-cleartext control arm replaced by the measured stores-nothing pin), sys_scim_provider untouched for #11757.", "gates": "44/44 derived families green at head dba7c1d (dispatch-gates.mjs, no paths passed, changed set from merge base 15bf9e859; three first-pass reds were unbuilt-workspace PREREQUISITE-class, re-run green after full packages build). Full suites at final shape: plugin-auth 81 files/1660 tests, plugin-security 86/1561, spec 435/11541, platform-objects 33/523, cli 197/2237, create-objectstack 14/177 - all green via os-verify-lock.sh, exit codes captured before pipes, verdict lines quoted in the PR body. Typechecks green for all six changed packages (their tsconfigs exclude *.test.ts - pre-existing posture, stated not hidden). Declared narrowing: docs-audit pair and release-rehearsal-clone excluded as CI-context runs (the docs-drift bot comment on PR 12726 is evidence its run happened). CI on the PR: two failures root-caused and fixed (da00d48, dba7c1d); remaining convergence is CI's own re-run on dba7c1d.", "files_changed": "39 paths vs merge base 15bf9e859: pnpm-workspace.yaml + pnpm-lock.yaml (exact 1.7.1 pin + honest comment rewrite), 8 new object files + identity index + i18n extract config + bundle-ownership list + 7 regenerated translation bundles (platform-objects), platform-object-names.ts (+8 registry names, spec), plugin-auth (package.json exact pin, auth-manager.ts, objectql-adapter.ts, scim-connection-service.ts NEW, manifest.ts, auth-schema-config.ts, sso-client-secret.ts, 3 gate tests), default-permission-sets.ts (+7 names, plugin-security), cli init.ts + init.test.ts, create-objectstack template + template-consistency.test.ts, scripts/check-prerelease-pin-watch.mjs, .changeset/scim-stable-migration.md.", "deviations": "Four files outside the claim's declared surface, all mechanical consequences, all named in the PR body: (1) plugin-security BETTER_AUTH_MANAGED_OBJECTS +7 - its bidirectional drift pin makes the list edit as unsplittable from declaring managedBy better-auth as the registry edit was from declaring the objects; this FALSIFIES the PM mechanism assumption that only the named surface is needed - same class as the registry finding that re-routed this card, and the honest alternative (omitting managedBy) would be gate-dodging metadata. (2) scripts/check-prerelease-pin-watch.mjs - the self-retirement leg its header promised, forced red on PR CI by the pin move itself. (3) platform-objects scripts/i18n-extract.config.ts + bundle-ownership.test.ts - the enumerated halves of the declared translation-bundle surface. (4) two stale-prose corrections naming the retired storeSCIMToken wiring as current. Also one standing-terms note: the ablation (cleartext mutation, grep-proof injected=1/original=0, blob 8bca1cf3->601d0c56, predicted 2-failed-3-passed == measured, restore proven by blob equality) needed no dist rebuild legs - the mutation target resolves via relative src imports under vitest, stated per the ablation contract.", "tests": "See gates field and the PR body's Verification record for verdict-line quotes; ablation record: mutation proven on disk before any verdict (grep counts + blob hash move), direction predicted then measured (2 failed | 3 passed), restore proven by blob equality to the HEAD blob plus marker absence, trap carried absolute paths. Post-fix causality checks: deadlocked dogfood file 13/13 in 19s after scoping (was 180s hook timeout); pin-watch self-test exit 1 reproduced locally before the fix, exit 0 after, check-self-test-workflow-commands green.", "mcp_calls": "10 - add_issue_comment (claim), issue_read get, issue_read get_comments, create_pull_request, pull_request_read get, get_job_logs x3 (pin-watch, typecheck-aggregator, dogfood), update_pull_request, add_issue_comment (this report).", "open_questions": [ { "question": "The platform auto-subscribed this session to PR 12726 with a drive-to-green stewardship posture; the dispatch contract (os-dev.md, maintainer 2026-08-10/2026-08-11 rulings) says the report lands at draft-PR time and CI convergence review is the PM's. I honored both where they overlap (two CI failures root-caused and fixed by push before this report); the subscription remains armed and will wake this session on future PR events. Who stewards PR 12726's remaining CI convergence?", "options": ["A - PM reviews CI convergence per the dispatch contract; this session answers further PR-event wakes only with small in-scope fixes", "B - PM archives this session after review and stewards the PR from the seat"], "recommendation": "A, because the session holds the full context of both CI fixes and the worktree; the dispatch contract still governs review and landing." }, { "question": "Adapter transaction scoping: real engine transactions only inside authenticated SCIM requests (AsyncLocalStorage marker from the verifier), core better-auth flows keep their historical sequential posture. Decided under the self-drive clause; recorded for review rather than asked.", "options": ["A - scoped design as landed", "B - unconditional native transactions (measured: deadlocks single-connection sqlite pools at showcase boot)", "C - fail-closed require:true (measured: 500s every sign-up on memory engines)"], "recommendation": "A on all four axes: real business need (upstream demands atomic PROVISIONING, which A delivers where it asked); long-term soundness (core-flow transactionality is its own engine-level design question, not a rider); AI-error-proofing (both rejected variants failed loudly in measurement, and A's scoping is documented at the decision site); startup scope (no platform-wide behavior change rides a dependency migration)." }, { "question": "Changeset level minor across the fixed version group, argued in the changeset body under the no-real-customers ruling (a major would move the entire platform for a default-off surface with zero consumers).", "options": ["A - minor as landed", "B - major"], "recommendation": "A; the maintainer floor (smooth upgrade) is satisfied at every commit and check-changeset-no-major passes; re-judging the level is one file if review disagrees." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 27, 2026
Review of record — PM ACCEPT + clause-② contract review PASS, both for PR #12726 at head
dba7c1d.Contract-review chain (dispatch-seat self-review path, 2026-08-21 ruling): eligibility per the machine reading — this seat's
external_metadata.last_served_model=claude-fable-5=CONTRACT_REVIEW_TIER(dispatch-gates.mjs:3813); the dispatch itself ran at that tier (cloud devsession_01EPEkvZe92KwvVUFhgdUR6S,claude-fable-5), so dispatched AND reviewed at tier.needs:contract-reviewis cleared in the same stroke as this PASS. Contract increment read from the diff, not the report — spot-checked atdba7c1d:sys_scimnames (sys_scim_providerSTAYS + 8 additive — seven stable models + the ObjectStack-owned credential store) — the chartered widening, in the ruled direction; bidirectional conformance keeps it honest.1.7.1in BOTH places (pnpm-workspace.yaml:179override target and plugin-auth's dependency literal) — the epic-owner pin ruling implemented as adopted at ACCEPT 5437430514; floating to 1.7.2 stays its own follow-up.KNOWN_UNMAPPED_MODELS = new Set<string>([])— shrunk to the EMPTY exact set with both assertion directions kept; nothing widened, which is the never-weaken constraint holding at its strongest possible reading.createHmac('sha256', secret)over a domain-separated input (scim-credential-v1:) — keyed digest, parity-or-better vs rc.1's unsalted SHA-256; the vacated upstream control arm replaced by pins of the vacating fact itself plus three wrong-shape negatives and live 401 rejection paths asserting the SCIM error envelope + status. Restructured honestly, not relaxed — exactly the stop-and-report fork's sanctioned alternative.connections: []+ app-ownedverifyBearerToken— connections stay runtime data;managedConnectionsNOT adopted (ruling honored).sys_scim_provideruntouched for Retiresys_scim_provider— consumer sweep first, no data migration (leg 1a of #11632, ruled on #11693) #11757. Smoothness: every table the installed library writes exists at this commit (atomic landing, as permitted).AsyncLocalStoragemarker from the verifier) is judged in contract: both alternatives were MEASURED broken (fail-closed → 275 red on memory engines; unconditional → dogfood sqlite pool deadlock, in CI and reproduced), and the landed scoping changes no core-flow behaviour — no platform-wide semantics ride a dependency migration. Dev open-question 2 resolved as its recommendation A.Deviations judged: the
BETTER_AUTH_MANAGED_OBJECTS+7 edit in plugin-security falsifies this seat's claim-time mechanism assumption ("only the named surface is needed") — the dev is right and the correction is accepted publicly: the #3325 bidirectional drift pin makes that list edit as unsplittable from honestmanagedBydeclarations as the registry edit was from the objects. The other three out-of-surface touches (pin-watch self-retirement leg forced red by this PR's own CI, the two enumerated translation-surface halves, two stale-prose corrections) are mechanical and named in the PR body with evidence. No gate-dodging, no silent widening.PM checklist: draft ✓ base
main✓Fixes #3653first line, no other closure-keyword adjacency (#11757/#11632 referenced without keywords) ✓ 39 files vs merge-base15bf9e8, governed-face scan: ZERO hits (no skills/.claude/adr/instruction files) — ordinary queue landing ✓ changeset present,minorargued under the no-real-customers ruling with check-changeset-no-major green (dev open-question 3 resolved as A) ✓ ablation predicted-then-measured with on-disk mutation proof and blob-equality restore ✓ two CI reds root-caused and fixed by push (not waved off) ✓ CI atdba7c1d: 40/40 check runs success (two path-conditional skips), including the very gate this PR's pin move forced red first.Landing: ready → merge queue now (every check green on the current head; clause-② enqueue gate satisfied: path limb + declared limb both covered by this at-tier review). File-disjoint from PR #12718's surface (measured — no shared generated artifacts between the two), so no regen relay is needed between them. Dev open-question 1 resolved as A: this seat stewards the PR to MERGED; the cloud session stays available for small in-scope fixes and is archived after MERGED per the cloud-card archive duty.
Generated by Claude Code
feat(auth): migrate @better-auth/scim to stable 1.7.1 — seven-model p…
feat(platform-objects)!: retire the sys_scim_provider platform object…
fix(cli): the scaffolded pnpm-workspace.yaml records the retired scim…
fix(cli): the scaffolded pnpm-workspace.yaml records the retired @bet…
docs(platform-objects): re-anchor the dead tracker citations to the c…