Skip to content

[measurement] better-auth admin 插件能否按动作粒度挂载(SCIM 只要 ban)——并同时评估是否需要升级 better-auth #14150

Description

@huangyiirene

由 #13816 的维护者裁决派生(2026-09-01,项目总监席批 #21)。维护者逐字:「13816 还要同时评估是否需要升级 better-auth」。

⛔ 这是一张只读测量卡,不写产品代码,不做升级 —— 交付物是本卡上的一份读数报告。它的答案决定 #13816 的形状 2(三态 + 构造期响亮冲突拒绝,已排队)是终态还是过渡态。

背景(一句话)

SCIM 开启会静默强制挂上 better-auth 的 admin 插件(impersonate-user / set-user-password / ban-user),耦合有 ADR-0071 背书:SCIM 的 active:false 停用路径经由 admin 的 ban。真修复(形状 1)是把依赖收窄到 SCIM 真正需要的动作 —— 可行性取决于 better-auth 插件的实际形状,没人测过。

要交付的三个读数

  1. 当前钉住版本的 admin 插件形状:better-auth 的 admin 插件能否按动作粒度挂载或裁剪?(插件 options 能否禁用端点子集?ban 能否脱离完整 admin 动作集单独获得?server-side API(auth.api.banUser 类)是否可以在不 mount HTTP 端点的情况下调用?)以源码/类型为准,不以文档想象。
  2. 升级评估(维护者点名追加):当前 monorepo 钉住的 better-auth 版本 vs 最新发布版 —— admin / SCIM / organization 插件区域的 changelog 差异;新版本是否提供更细的挂载粒度或解耦手段;若需升级,破坏面与迁移成本的量级读数(peer 依赖、schema 迁移、API 变名)。
  3. 结论分叉(具名、可证伪):

⚠️ 若结论是「升级才可行」:升级本身回决策箱裁(成本读数随卡附上),⛔ 本卡不自行升级。

Refs: #13816(裁决与形状 2 实施卡)· #13439(已裁姊妹卡:三键声明)· ADR-0071 · objectstack-ai/cloud#1451。

Activity

  1. added theissue type on Sep 1, 2026
  2. self-assigned this
    on Sep 2, 2026
  3. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 1 (services seat)
    Session: session_01AUF1NoViznQK32gqpK8wS8
    Branch: claude/issue-14150-better-auth-admin-granularity-measurement (pushed empty as the claim's landing marker — this is a measurement-only card; the deliverable is a reading report on this card, no PR expected)
    Worktree: objectstack-issue-14150
    Domain: domain:services
    File surface: READ-ONLY — packages/plugins/plugin-auth/**, the installed better-auth / @better-auth/* sources and type declarations, pnpm-lock.yaml, and the upstream changelog; ⛔ zero writes to product code, ⛔ no dependency upgrade (stop on breach; explain in the report)
    Container & model: S–M measurement, mode:subagent, model: opus — dispatch-gates at 72adb7fc prints "no path-derived mandate"; judgment tier because the three readings decide #13816's final shape
    Clause-②: no
    Serial constraints cleared: read-only card, no single-writer path claimed; #14157 (in flight) writes plugin-auth/src/auth-plugin.ts — this card only reads it. Pinned versions read on origin/main: better-auth and @better-auth/{core,oauth-provider,scim,sso} all ^1.7.2 in packages/plugins/plugin-auth/package.json.


    Generated by Claude Code

  4. claude commented on Sep 2, 2026

    @claude
    Contributor

    Claim: dev seat (measurement) — session session_01AUF1NoViznQK32gqpK8wS8, scratchpad isolation key issue-14150-knife1 (shared session id with the PM claim above; this comment is the dev seat's own claim on the same session).
    Branch: claude/issue-14150-better-auth-admin-granularity-measurement — pushed empty as the landing marker. No commits, no PR expected (read-only measurement card).
    Worktree: objectstack-issue-14150.

    Scope as dispatched: the three readings on this card (admin plugin shape at the pinned/resolved better-auth version; upgrade evaluation vs the latest published release; the named falsifiable fork). READ-ONLY — packages/plugins/plugin-auth/**, installed better-auth / @better-auth/* sources and .d.ts, pnpm-lock.yaml, upstream changelog. No product code changes, no dependency upgrade, no PR. objectstack-ai/cloud#1451 is outside this session's GitHub scope and will be recorded as NOT MEASURED rather than guessed.

    Re-read the thread before starting: the only prior comment is the PM claim on the same session/branch — no race.


    Generated by Claude Code

  5. claude commented on Sep 2, 2026

    @claude
    Contributor

    os-dev-report

    Measurement report — better-auth admin action granularity, and the upgrade question

    Read-only card. No product code changed, no dependency moved, no PR. Branch claude/issue-14150-better-auth-admin-granularity-measurement was pushed empty as the claim's landing marker; node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack reports "this branch changes nothing against origin/main (merge base e854a53)", which is the mechanical proof that this seat wrote nothing. All readings taken on origin/main at e854a531a in worktree objectstack-issue-14150 after pnpm install.

    Verdict, up front: FEASIBLE — and on the currently pinned version, with no upgrade. The reason is stronger than the card supposed, and it moves the fork: at the installed vendor version SCIM does not need ban at all, because the vendor removed the coupling the forcing was justified by.


    Premise check (before the readings)

    Premise A — "SCIM enablement silently forces the admin plugin on today": HOLDS.

    • packages/plugins/plugin-auth/src/auth-manager.ts:2494 — const scimEffective = resolveScimEnabled(pluginConfig);
    • :2513 — admin: pluginConfig.admin ?? scimEffective, (the mount)
    • :5149 — admin: pluginConfig.admin ?? resolveScimEnabled(pluginConfig), (the advertised-feature surface)
    • :331-334 — resolveScimEnabled = pluginConfig?.scim ?? readBooleanEnv('OS_SCIM_ENABLED') ?? false

    #13816's Shape 2 landed (PR #14246) and refines this but does not remove it: an explicit plugins.admin: false beside effective SCIM is now refused loudly (:353-367, assertScimAdminCoherence), while an absent admin is still silently forced on by SCIM. So the amplification the card is about is intact.

    Premise B — "SCIM active:false deactivation routes through admin's ban": FALSE at the pinned version. This is the load-bearing finding of the card and it is what makes the fork resolvable.

    @better-auth/scim@1.7.2 contains no ban code path at all. The substring ban occurs zero times in the entire published package:

    grep -ro "ban" node_modules/@better-auth/scim/dist | wc -l   →   0
    

    What active: false does instead — dist/index.mjs:4196-4231 (reconcileUser): it calls the OPTIONAL host callback options.identity.reconcileUser(state, ...) (typed at dist/index.d.mts:267 — "Reconciles global enabled or disabled state inside the SCIM transaction."), then if (!active) await input.auth.internalAdapter.deleteUserSessions(userId). plugin-auth passes no identity option — the only two members handed to scim() are connections: [] and authentication.verifyBearerToken (auth-manager.ts:3256-3270) — and reconcileUser appears nowhere in this repo's sources (git grep -n reconcileUser -- 'packages/**/src/**' → 0 hits).

    The coupling was real and was removed upstream. Measured on published tarballs, not recalled:

    @better-auth/scim ban occurrences in dist/ deactivation path
    1.6.30 15 writes banned: true via resolveSCIMActiveDeactivation
    1.7.0-rc.1 — this repo's pin until 2026-08-27 17 same, and refuses when ctx.context.hasPlugin("admin") is false
    1.7.0 0 host callback + deleteUserSessions
    1.7.1 0 same
    1.7.2 — installed 0 same

    1.6.30 dist/index.mjs:751-755, verbatim, is the sentence this repo's comments encode:

    "Applies SCIM active semantics to a pending user update. active maps to the admin plugin's banned field, the only enforced disabled-user state in Better Auth, so honoring deactivation requires the admin plugin. ... A deactivation without the admin plugin is rejected, never silently dropped."

    Premise C — "the coupling has ADR-0071 backing": the cited record is about something else. docs/adr/0071-dataset-semantic-layer-depth.md is "Dataset semantic-layer depth — multi-hop joins". 39 files cite ADR-0071 with the SCIM/identity meaning; no such record exists. Filed as #14361; it matters here because the ruled fork's "ADR-0071 revision card", cut literally, would revise the dataset record.


    Reading 1 — the admin plugin's shape at the resolved version

    Version. Pinned ^1.7.2 for better-auth and @better-auth/{core,oauth-provider,scim,sso} (packages/plugins/plugin-auth/package.json:27-37). Resolved 1.7.2 in pnpm-lock.yaml (:5562 for better-auth, :9617/:9632/:9641 for the scoped three), and the installed package.json of each of the five reports 1.7.2.

    Q1 — can the plugin's options disable a subset of its endpoints? No.

    admin() builds a fixed object literal of 15 endpoints, unconditional, with no branch on options — better-auth/dist/plugins/admin/admin.mjs:66-82:

    setRole, getUser, createUser, adminUpdateUser, listUsers, listUserSessions,
    unbanUser, banUser, impersonateUser, stopImpersonating, revokeUserSession,
    revokeUserSessions, removeUser, setUserPassword, userHasPermission
    

    Reverse control — the complete option surface I enumerated, dist/plugins/admin/types.d.mts, interface AdminOptions, all 11 members: defaultRole, adminRoles, defaultBanReason, defaultBanExpiresIn, impersonationSessionDuration, schema, ac, roles, adminUserIds, bannedUserMessage, allowImpersonatingAdmins. Not one of them takes an endpoint name, an allowlist, a denylist or a feature flag. The "no" is a property of that enumeration, not an impression.

    Q2 — can ban-user be obtained without the full admin action set? Yes — three routes, measured; two of them real.

    Route A — plugin options: NOT AVAILABLE (Q1).

    Route B — the top-level disabledPaths option: AVAILABLE, HTTP only. @better-auth/core/dist/types/init-options.d.mts:1497 declares disabledPaths?: string[] on BetterAuthOptions ("Paths you want to disable."). It is enforced in the router's onRequest, better-auth/dist/api/index.mjs:164-166:

    const disabledPaths = ctx.options.disabledPaths || [];
    const normalizedPath = normalizePathname(req.url, basePath);
    if (disabledPaths.includes(normalizedPath)) return new Response("Not Found", { status: 404 });
    

    and the OpenAPI generator honours it too (dist/plugins/open-api/generator.mjs:427, :475), so disabled routes also leave the published docs. Admin paths are the literal strings at dist/plugins/admin/routes.mjs: /admin/set-role :44, /admin/get-user :81, /admin/create-user :134, /admin/update-user :233, /admin/list-users :324, /admin/list-user-sessions :403, /admin/unban-user :449, /admin/ban-user :508, /admin/impersonate-user :559, /admin/stop-impersonating :630, /admin/revoke-user-session :669, /admin/revoke-user-sessions :712, /admin/remove-user :755, /admin/set-user-password :804, /admin/has-permission :871. Fail direction: denylist ⇒ fail-OPEN — an endpoint a future vendor version adds is mounted unless someone remembers to add it to the list.

    Route C — narrowing the plugin object's own endpoints record: AVAILABLE, and already this repo's practice. BetterAuthPlugin.endpoints is a plain optional record (@better-auth/core/dist/types/plugin.d.mts:37), and getEndpoints merges it by spread with no validation (better-auth/dist/api/index.mjs:86-90). ObjectStack already mutates that record on this exact plugin: applyPlatformAdminImpersonation replaces plugin.endpoints.impersonateUser in place (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts:198, called from auth-manager.ts:2985), and its own docblock states the property Shape 1 would rely on — "Returns the SAME plugin object (mutated), so the plugin's id, schema, hooks, $ERROR_CODES and every other endpoint stay exactly as the vendor built them". Deleting keys instead of replacing one is the same mechanism. Fail direction: allowlist ⇒ fail-CLOSED, and it narrows the inferred auth.api surface as well as the HTTP one.

    Route D — the ac / roles access-control statements: authorization, not mounting. dist/plugins/admin/access/statement.mjs declares user: ["create","list","set-role","ban","impersonate","impersonate-admins","delete","set-password","set-email","get","update"] and session: ["list","revoke","delete"]; banUser checks hasPermission({ ..., permissions: { user: ["ban"] } }) (routes.mjs:530). A custom ac/roles can deny every statement but ban — the endpoints stay mounted and answer FORBIDDEN. This is a real narrowing of effect, not of surface, and it is largely moot here: #9652 already records that this vendor gate refuses every ObjectStack platform admin, because it reads the legacy user.role === 'admin' scalar ADR-0068 D2 stopped synthesizing.

    Q3 — can the server-side API be invoked without mounting the HTTP endpoints? Yes.

    createBetterAuth builds api from getEndpoints(authContext, options) at dist/auth/base.mjs:9 and returns it at :44; the router is constructed separately, only inside the request handler, at :38. disabledPaths lives in the router's onRequest. So with every /admin/* path in disabledPaths, auth.api.banUser(...) still resolves and runs while every admin HTTP path answers 404.

    Caveat that must travel with that yes: every admin route mounts adminMiddleware (routes.mjs:17-21), which calls getAuthoritativeSessionFromCtx and throws UNAUTHORIZED without a session. So auth.api.banUser is a headers-carrying server-side call, not an unauthenticated internal seam. For a purely internal ban ObjectStack already has the better instrument: its own write in admin-ban-endpoints.ts.

    What the forced admin plugin actually exposes in THIS repo today

    Of the vendor's 15 endpoints, ObjectStack already shadows or replaces 7. Its raw Hono mounts are registered before the catch-all that delegates to better-auth (auth-plugin.ts:2801 rawApp.all(basePath + '/*')), so they win: create-user :2237, ban-user :2306, unban-user :2327, remove-user :2399, has-permission :2452, set-user-password :2487; and impersonate-user is re-gated in place on the ADR-0068 predicate.

    The vendor-served residue is 8: set-role, get-user, update-user, list-users, list-user-sessions, stop-impersonating, revoke-user-session, revoke-user-sessions — all still gated on the legacy scalar (#9652).

    ⇒ Two of the three actions the finding names (ban-user, set-user-password) are already ObjectStack mounts behind the ADR-0068 gate, and the third (impersonate-user) is the vendor endpoint re-authorized in place. That does not make the forcing harmless — the 8 residual routes are mounted, and any deployment carrying a persisted legacy role='admin' row reaches them — but it does mean the fix is smaller than "replace the plugin".

    The residual dependency after Shape 1

    What ObjectStack genuinely still needs from admin() is not its endpoints: it is the plugin's schema (the banned / banReason / banExpires / role columns, mapped by buildAdminPluginSchema() at auth-manager.ts:2969-2971) and its init() hooks (admin.mjs:25-52) — the session-create hook that refuses a banned user with BANNED_USER and auto-lifts expired bans. auth-plugin.ts:2288-2303 already encodes exactly this dependency: without the plugin, a ban write would be "a declared-but-not-enforced ban: the console reports success and the banned user signs straight back in", so the ObjectStack ban routes answer 501 when auth.api.banUser is absent. Route C keeps schema and init() untouched by construction, which is why it is the only route that answers the card's question without breaking that.


    Reading 2 — the upgrade evaluation

    There is no upgrade. The repo is already on the newest published release of every better-auth package. Read from the npm registry (reachable through the session proxy; https://registry.npmjs.org/..., HTTP 200 on all five):

    package dist-tags.latest published pinned / resolved here
    better-auth 1.7.2 2026-08-26T19:06:23Z ^1.7.2 / 1.7.2
    @better-auth/core 1.7.2 2026-08-26T19:13:25Z ^1.7.2 / 1.7.2
    @better-auth/scim 1.7.2 2026-08-26T19:03:23Z ^1.7.2 / 1.7.2
    @better-auth/sso 1.7.2 2026-08-26T19:03:23Z ^1.7.2 / 1.7.2
    @better-auth/oauth-provider 1.7.2 2026-08-26T19:03:23Z ^1.7.2 / 1.7.2

    No newer stable exists; the other dist-tags are all older lines, not newer ones (beta 1.7.0-beta.10, rc 1.7.0-rc.6, release-1.4 1.4.22, canary 1.0.0-canary.14). So:

    • Changelog delta between pinned and latest: empty set. There are zero releases between them.
    • Breaking surface of an upgrade: none, because there is no upgrade. Peer deps, schema migrations and renamed APIs are all not-applicable. Migration cost is zero because the migration already happened — 03520ebef (2026-08-19) moved the family onto stable ^1.7.1, 366f89576 (2026-08-27) migrated @better-auth/scim to stable, 9c4c431fd (2026-08-31) moved the family to 1.7.2.
    • Does a newer version offer finer mount granularity? The question is unanswerable in the affirmative and moot in the negative: the granularity that exists (disabledPaths, the endpoint record, ac) is what 1.7.2 has, and 1.7.2 is the frontier.

    The delta that does matter is behind us, not ahead. The 1.7.0 release is where the admin/ban coupling left @better-auth/scim (the tarball table in Premise B). The upstream release notes for v1.7.0 corroborate a decoupling programme in that area — "Decoupled SCIM provisioning from the organization plugin", "Isolated SCIM provider connections from the organization and SSO plugins" — though they do not call out the admin/ban removal by name. ⚠️ Evidence grade: that bullet list came from a prompt-summarised fetch of the rendered GitHub release page and is corroboration only; the decisive evidence is the tarball measurement, which is direct and reproducible. api.github.com/repos/better-auth/better-auth/... is NOT MEASURED — HTTP 403, "GitHub access to this repository is not enabled for this session" (the session proxy is bound to this org's repositories).

    ⇒ No upgrade decision needs to go back to the maintainer's inbox. The card's contingency ("if feasibility requires an upgrade, the upgrade decision itself goes back") does not fire.


    Reading 3 — the named, falsifiable fork

    FEASIBLE. Ban is obtainable without the full admin action set, on the currently pinned version, with no upgrade. Per the ruled fork that means: Shape 1 is live, #13816's Shape 2 becomes transitional, and an ADR revision is owed — with two corrections the ruling could not have anticipated, both measured above.

    Correction 1 — Shape 1 is not "narrow admin to ban". At 1.7.2, SCIM needs nothing from admin. The vendor dependency the forcing exists to satisfy no longer exists (Premise B). "Narrow the dependency to ban" was the right question against 1.7.0-rc.1; against 1.7.2 the honest shape is drop the forcing and let a SCIM deployment decline the admin surface outright. That is strictly stronger than what the fork promised, and it retires the #13816 refusal along with the contradiction it refuses.

    Correction 2 — dropping the forcing must be decided together with #14360, not before it. Because the vendor stopped writing banned, SCIM active: false today revokes sessions and disables nothing, and the #5892 break-glass guard's SCIM trigger is unreachable. So the platform faces one design question, not two:

    • If SCIM deprovisioning should disable the account, the implementation is the vendor's identity.reconcileUser host hook calling ObjectStack's own ban write — which admin-ban-endpoints.ts already owns, complete with the last-admin guard. That still needs the admin plugin's schema and init() hooks (for the column and the sign-in refusal), but none of its 15 endpoints. That is precisely "ban without the full admin action set", reached through the door the vendor opened when it closed the old one.
    • If session revocation is deemed sufficient, the forcing has no justification at all and simply goes.

    Either branch narrows the mounted surface; they differ only in whether the ban write comes back.

    The ADR revision card cannot be cut as "revise ADR-0071" — see #14361. The record that number names is about dataset multi-hop joins; the SCIM/identity decision has never been written down here.

    Recommendation, on the four axes — long-term soundness leading

    Recommended: cut Shape 1 as "stop forcing admin under SCIM; if a disable is wanted, implement identity.reconcileUser over ObjectStack's own ban write", and give it Route C (narrow the plugin's endpoints record) as the mechanism for any admin surface that remains. Demote Shape 2 to transitional. Write the missing SCIM/identity ADR rather than revising ADR-0071.

    • Long-term soundness for this project (the leading axis). The current state is a forced permission surface justified by a vendor behaviour that no longer exists — the definition of a coupling nobody can re-derive. Route C is fail-closed and is already the repo's own landed technique on this very plugin; Route B is a denylist that silently re-opens on the next vendor bump, which is the failure mode this codebase spends gates preventing. Removing a forced mount is a contraction of the declared surface, not an expansion. Writing the ADR that 39 citations already assume closes a documentation hole a gate structurally cannot see.
    • Real business need. Measured, not imagined: SCIM is a paid Identity-lifecycle capability (auth-manager.ts:3230-3232); objectstack-ai/cloud#1451 is parked on this chain; [finding] SCIM forces the better-auth admin plugin on (pluginConfig.admin ?? scimEffective) and a deployment has no declared way to decline it — split from #13439 by the 2026-08-31 ruling #13816 records a deployment that wants directory sync and not an admin console. The pull is a real deployment shape, not a hypothetical.
    • Making AI-written code hard to get wrong. Today an app author reading the generated auth-config.mdx is told admin is "forced on by effective SCIM (ADR-0071)" and, following the pointer, lands on a dataset ADR. Removing the forcing removes the surprise; an allowlisted endpoint record makes the mounted set a declared fact rather than a subtraction someone must maintain. Route B would leave the author with a denylist to keep in their head.
    • Startup scope discipline. This is subtraction: fewer mounted endpoints, one fewer forced dependency, one fewer conflict refusal to explain. The only addition is the reconcileUser hook, and only on the branch where the platform decides deprovisioning must disable — and that hook reuses a write that already exists. No speculative surface is added on either branch.

    The three axes together do not outweigh the first here; they point the same way. What is not mine to decide and is put to triage: whether SCIM deprovisioning must disable accounts (#14360), which determines which of the two Shape-1 branches is cut.


    NOT MEASURED

    • objectstack-ai/cloud#1451 — that repository is outside this session's GitHub scope. Not read, not guessed.
    • Upstream better-auth release notes via the GitHub API — HTTP 403 from the session proxy on https://api.github.com/repos/better-auth/better-auth/releases?per_page=5. A rendered-page fetch was used instead and is flagged as corroboration only.
    • Runtime behaviour. Everything above is read from installed vendor sources, published tarballs, the npm registry and this repo's tree. No better-auth instance was constructed and no request was issued, so disabledPaths and the endpoint-record narrowing are measured as source facts, not as observed 404s. An implementation card should pin both with a test.
    • Whether any deployment currently sets plugins.admin explicitly. Config-time fact, not visible in this repo.

    Out-of-scope findings filed (unassigned, deduped first)

    Dedup method: full REST enumeration of all 455 open issues (477 open items across 5 pages, PRs excluded, count reconciled against open_issues_count) plus local grep for 0071, deprovision, active: false, reconcileUser; and one targeted search_issues, whose empty result was validated by a control query in the same session that returned 13 known hits including #13816 and #14150.

    Machine-checkable report

    {
      "issue": 14150,
      "status": "done",
      "branch": "claude/issue-14150-better-auth-admin-granularity-measurement",
      "pr": null,
      "files_changed": 0,
      "premise_still_valid": false,
      "summary": "Read-only measurement delivered as the report comment on #14150; no product code, no dependency move, no PR. Premise A holds (effective SCIM still forces the admin plugin on - auth-manager.ts:2494/:2513/:5149, with #13816's landed refusal covering only an EXPLICIT admin:false). Premise B is FALSE at the pinned version: @better-auth/scim@1.7.2 contains zero occurrences of the substring 'ban' in its whole dist; active:false calls the optional host hook identity.reconcileUser (which plugin-auth does not pass - auth-manager.ts:3256-3270, zero reconcileUser hits repo-wide) and then deleteUserSessions. Tarball measurement dates the removal: 1.6.30 = 15 ban occurrences, 1.7.0-rc.1 (this repo's pin until 2026-08-27) = 17, 1.7.0/1.7.1/1.7.2 = 0. Premise C is also off: the cited ADR-0071 record is 'Dataset semantic-layer depth - multi-hop joins'. Fork = FEASIBLE on the current version with NO upgrade: the npm registry reports dist-tags.latest = 1.7.2 for all five better-auth packages, which is exactly what is pinned and resolved, so the upgrade contingency does not fire.",
      "tests": "No build or test suite was run and none is owed: zero files changed. Mechanical proof - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` in the worktree prints 'this branch changes nothing against origin/main (merge base e854a531a) - nothing to derive' (exit 2, its no-changes verdict, checked against this checkout's origin), and `git status --porcelain` is empty. Measurement commands with their readings, all after `pnpm install` (exit 0) in worktree objectstack-issue-14150 at e854a531a: (1) installed versions - node_modules/{better-auth,@better-auth/core,@better-auth/scim,@better-auth/sso,@better-auth/oauth-provider}/package.json all report 1.7.2; pnpm-lock.yaml:5562/:9617/:9632/:9641. (2) `grep -ro \"ban\" node_modules/@better-auth/scim/dist | wc -l` = 0 on 1.7.2, 1.7.1 and 1.7.0; = 17 on the 1.7.0-rc.1 tarball and 15 on 1.6.30 (tarballs fetched from registry.npmjs.org into the scratchpad, extracted, grepped - each version measured with the same command). (3) `git grep -n reconcileUser -- 'packages/**/src/**'` = 0 hits. (4) npm registry dist-tags read over HTTP 200 for all five packages. (5) api.github.com/repos/better-auth/better-auth/releases = HTTP 403 (session proxy is repo-bound) - recorded NOT MEASURED, a rendered-page fetch used as flagged corroboration only. (6) Source citations for every claim about mount granularity are file:line into the installed dist and .d.mts, listed in the report comment.",
      "mcp_calls": "2 - two mcp__github__search_issues calls (one dedup query, one control query that returned 13 known hits and validated the empty first result). Everything else went over the container's REST channel (repo-scoped probe green) or git; card and comment reads, both finding filings and the report comment are REST.",
      "open_questions": [
        {
          "question": "Should SCIM active:false disable the account, or is session revocation sufficient? This is now a live question rather than a settled one, because the vendor stopped writing `banned` in 1.7.0 and nothing in this repo replaced it (filed as #14360). It decides which of the two Shape-1 branches the PM cuts.",
          "options": [
            "A - Disable: implement the vendor's identity.reconcileUser host hook over ObjectStack's OWN ban write (admin-ban-endpoints.ts already owns it, last-admin guard included). Needs the admin plugin's schema + init() hooks for the column and the sign-in refusal, but NONE of its 15 endpoints.",
            "B - Session revocation only: accept today's behaviour, drop the admin forcing entirely, and restate the coupling everywhere it is currently asserted (auth-manager.ts:2489/:353-367, generated auth-config.mdx:105/:215, the #5892 break-glass suite whose SCIM trigger is now unreachable)."
          ],
          "recommendation": "A, because long-term soundness leads: 'declared and enforced' is this codebase's governing constraint (ADR-0049), and a directory that says 'deactivated' while the account still signs in is the false-surface shape that constraint exists to forbid. A also costs less than it looks - the write, the envelope and the last-admin guard already exist; only the hook is new. B is the honest choice only if the maintainer decides directory deactivation is out of the product's scope, and it still owes the same restatement work."
        },
        {
          "question": "The ruled fork says a FEASIBLE reading produces 'an ADR-0071 revision card'. ADR-0071 in this repo is 'Dataset semantic-layer depth - multi-hop joins'; the SCIM/identity decision 39 files cite under that number has never been written here (filed as #14361). What should the ADR card actually be?",
          "options": [
            "A - Write the missing SCIM/identity-lifecycle ADR under a free number and re-point the 39 citations to it.",
            "B - If the decision lives in objectstack-ai/cloud, adopt the sibling-repo spelling check-adr-anchors already supports (a repo qualifier before the id) so a bare ADR-0071 stops meaning two things.",
            "C - Revise ADR-0071 itself - i.e. accept that the dataset record is the SCIM record."
          ],
          "recommendation": "A, unless the maintainer knows a cloud-side record exists, in which case B. C is wrong on its face: the two subjects share nothing, and merging them would make the dataset lint rule's own citation (packages/lint/src/validate-dataset-references.ts:220) point at a security decision. Not filed as an implementation card - the fork's follow-ups are the PM's to cut."
        }
      ],
      "out_of_scope_findings": [
        "filed as #14360: SCIM active:false no longer disables the account - the vendor ban coupling left @better-auth/scim in 1.7.0 and nothing in this repo replaced it; also names the now-unreachable #5892 break-glass SCIM trigger",
        "filed as #14361: the SCIM/identity ADR-0071 citation resolves to the dataset semantic-layer record - 39 files, invisible to check-adr-anchors because the number does resolve"
      ]
    }

    Generated by Claude Code

  6. os-sales commented on Sep 2, 2026

    @os-sales
    Collaborator

    ACCEPT — measurement delivered as the os-dev-report comment (14150#issuecomment-5503768456); zero files changed, no PR, nothing to land. PM seat, domain:services, session_01AUF1NoViznQK32gqpK8wS8.

    Verified against the card's three deliverables, from the report's file:line citations into the installed dist and the registry reads, not from its prose:

    1. Admin-plugin shape at the pinned version — plugin options cannot prune endpoints (15-endpoint literal, admin.mjs:66-82; reverse control: all 11 AdminOptions members enumerated, none endpoint-related). ban IS obtainable alone by two real routes: top-level disabledPaths (router-side, HTTP-only, fail-open denylist) and narrowing the plugin's own endpoints record (fail-closed allowlist; this repo's landed technique at admin-impersonate-endpoint.ts:198, keeps init()/schema). auth.api.banUser survives either.
    2. Upgrade evaluation — no upgrade exists: dist-tags.latest is 1.7.2 for all five better-auth packages, exactly what is pinned and resolved. The upgrade contingency does not fire.
    3. Fork — FEASIBLE on the current version with no upgrade.

    premise_still_valid: false on two premises, and that is a good run, not a failure: (B) the SCIM→ban coupling left @better-auth/scim at 1.7.0 (tarball-measured: 15 / 17 / 0 / 0 / 0 occurrences across 1.6.30 → 1.7.2), so "SCIM only needs ban" presumes a vendor path that no longer exists — the reachable seam is the identity.reconcileUser host callback; (C) ADR-0071 in this repo is the dataset semantic-layer record. Triage on #14360 independently noted the first re-framing.

    Follow-ups cut from the fork (the "shape-1 implementation card + ADR-0071 revision card" the card names):

    Recorded, not counted: GitHub releases API answered 403 through the session proxy (NOT MEASURED; a rendered page used only as flagged corroboration). MCP read discipline: two search_issues calls, one of them a control — noted per the review checklist, ACCEPT unaffected.

    This card closes as completed in the next stroke, together with the #14360 unlock (pm:blocked → pm:queue, premise re-verified by triage on 8094834), and pm:dispatched / pm:blocking are stripped with the close.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions