Skip to content

SCIM: 停在 @better-auth/scim rc.1,等正式版再整体迁移 —— rc.2 换掉了整套模型 #3653

Closed

Description

@os-zhuang

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 的部署,今天不要让 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)。到那时一并处理,作为一个独立的迁移任务:

  1. 按正式版的模型集补齐平台对象(届时以正式版为准,不是 rc.2);
  2. 决定 sys_scim_provider 的去向 —— 迁移还是废弃;
  3. 重做连接配置模型(静态 connections vs 现有的 generate-token + Setup UI);
  4. 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 现在读前者、套后者,算出它们真正会写的列。

Refs #3624, #3647, #3688, ADR-0071。


Generated by Claude Code

Activity

  1. changed the title [-]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[/+] on Jul 27, 2026
  2. os-zhuang commented on Jul 27, 2026

    @os-zhuang
    ContributorAuthor

    补充:这不只是"缺四张表",spec 已经在对外宣称 group 供给

    顺着 #3688 的 docs-drift 提示查下去,发现缺口比正文写的更靠前一层 —— packages/spec 声明了 SCIM group 的类型,但没有任何运行时消费它们:

    packages/spec/src/identity/scim.zod.ts:324  SCIMGroupReferenceSchema
    packages/spec/src/identity/scim.zod.ts:685  SCIMGroupSchema
    packages/spec/src/identity/scim.zod.ts:735  type SCIMGroup
    packages/spec/src/identity/scim.zod.ts:775  ListResponse.Resources 里联合了 SCIMGroupSchema
    

    对 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 就撞到不存在的表。

    注:content/docs/references/ 是自动生成的(AGENTS.md 文档守则),所以无论选哪条路都不能手改那份文档 —— 它会随 spec 自动跟上。


    Generated by Claude Code

  3. changed the title [-]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 换掉了整套模型[/+] on Jul 27, 2026
  4. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    存量裁决轮(维护者 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

  5. 40 remaining items

  6. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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

  7. os-sales commented on Aug 27, 2026

    @os-sales
    Collaborator

    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 dev session_01EPEkvZe92KwvVUFhgdUR6S, claude-fable-5), so dispatched AND reviewed at tier. needs:contract-review is cleared in the same stroke as this PASS. Contract increment read from the diff, not the report — spot-checked at dba7c1d:

    • Registry: 9 sys_scim names (sys_scim_provider STAYS + 8 additive — seven stable models + the ObjectStack-owned credential store) — the chartered widening, in the ruled direction; bidirectional conformance keeps it honest.
    • Pin: exact 1.7.1 in BOTH places (pnpm-workspace.yaml:179 override 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.
    • Parity gate: 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.
    • Credential-at-rest: 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.
    • Constructor: stable shape with connections: [] + app-owned verifyBearerToken — connections stay runtime data; managedConnections NOT adopted (ruling honored). sys_scim_provider untouched for Retire sys_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).
    • The transaction-scoping design (native adapter transactions ONLY inside authenticated SCIM requests, AsyncLocalStorage marker 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 honest managedBy declarations 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 #3653 first line, no other closure-keyword adjacency (#11757/#11632 referenced without keywords) ✓ 39 files vs merge-base 15bf9e8, governed-face scan: ZERO hits (no skills/.claude/adr/instruction files) — ordinary queue landing ✓ changeset present, minor argued 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 at dba7c1d: 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

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions