Repository navigation
[measurement] better-auth admin 插件能否按动作粒度挂载(SCIM 只要 ban)——并同时评估是否需要升级 better-auth #14150
Description
Activity
- addedpriority:p1High: required for production / M2High: required for production / M2
on Sep 1, 2026 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 installedbetter-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 at72adb7fcprints "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) writesplugin-auth/src/auth-plugin.ts— this card only reads it. Pinned versions read onorigin/main:better-authand@better-auth/{core,oauth-provider,scim,sso}all^1.7.2inpackages/plugins/plugin-auth/package.json.
Generated by Claude Code
Claim: dev seat (measurement) — session
session_01AUF1NoViznQK32gqpK8wS8, scratchpad isolation keyissue-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/**, installedbetter-auth/@better-auth/*sources and.d.ts,pnpm-lock.yaml, upstream changelog. No product code changes, no dependency upgrade, no PR.objectstack-ai/cloud#1451is 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
os-dev-report
Measurement report — better-auth
adminaction granularity, and the upgrade questionRead-only card. No product code changed, no dependency moved, no PR. Branch
claude/issue-14150-better-auth-admin-granularity-measurementwas pushed empty as the claim's landing marker;node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackreports "this branch changes nothing against origin/main (merge base e854a53)", which is the mechanical proof that this seat wrote nothing. All readings taken onorigin/mainate854a531ain worktreeobjectstack-issue-14150afterpnpm 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
banat all, because the vendor removed the coupling the forcing was justified by.
Premise check (before the readings)
Premise A — "SCIM enablement silently forces the
adminplugin 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: falsebeside effective SCIM is now refused loudly (:353-367,assertScimAdminCoherence), while an absentadminis still silently forced on by SCIM. So the amplification the card is about is intact.Premise B — "SCIM
active:falsedeactivation 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.2contains no ban code path at all. The substringbanoccurs zero times in the entire published package:grep -ro "ban" node_modules/@better-auth/scim/dist | wc -l → 0What
active: falsedoes instead —dist/index.mjs:4196-4231(reconcileUser): it calls the OPTIONAL host callbackoptions.identity.reconcileUser(state, ...)(typed atdist/index.d.mts:267— "Reconciles global enabled or disabled state inside the SCIM transaction."), thenif (!active) await input.auth.internalAdapter.deleteUserSessions(userId).plugin-authpasses noidentityoption — the only two members handed toscim()areconnections: []andauthentication.verifyBearerToken(auth-manager.ts:3256-3270) — andreconcileUserappears 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/scimbanoccurrences indist/deactivation path 1.6.3015 writes banned: trueviaresolveSCIMActiveDeactivation1.7.0-rc.1— this repo's pin until 2026-08-2717 same, and refuses when ctx.context.hasPlugin("admin")is false1.7.00 host callback + deleteUserSessions1.7.10 same 1.7.2— installed0 same 1.6.30dist/index.mjs:751-755, verbatim, is the sentence this repo's comments encode:"Applies SCIM
activesemantics to a pending user update.activemaps to the admin plugin'sbannedfield, 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.mdis "Dataset semantic-layer depth — multi-hop joins". 39 files citeADR-0071with 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.2forbetter-authand@better-auth/{core,oauth-provider,scim,sso}(packages/plugins/plugin-auth/package.json:27-37). Resolved1.7.2inpnpm-lock.yaml(:5562forbetter-auth,:9617/:9632/:9641for the scoped three), and the installedpackage.jsonof each of the five reports1.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, userHasPermissionReverse 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-userbe 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
disabledPathsoption: AVAILABLE, HTTP only.@better-auth/core/dist/types/init-options.d.mts:1497declaresdisabledPaths?: string[]onBetterAuthOptions("Paths you want to disable."). It is enforced in the router'sonRequest,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 atdist/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
endpointsrecord: AVAILABLE, and already this repo's practice.BetterAuthPlugin.endpointsis a plain optional record (@better-auth/core/dist/types/plugin.d.mts:37), andgetEndpointsmerges it by spread with no validation (better-auth/dist/api/index.mjs:86-90). ObjectStack already mutates that record on this exact plugin:applyPlatformAdminImpersonationreplacesplugin.endpoints.impersonateUserin place (packages/plugins/plugin-auth/src/admin-impersonate-endpoint.ts:198, called fromauth-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_CODESand 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 inferredauth.apisurface as well as the HTTP one.Route D — the
ac/rolesaccess-control statements: authorization, not mounting.dist/plugins/admin/access/statement.mjsdeclaresuser: ["create","list","set-role","ban","impersonate","impersonate-admins","delete","set-password","set-email","get","update"]andsession: ["list","revoke","delete"];banUsercheckshasPermission({ ..., permissions: { user: ["ban"] } })(routes.mjs:530). A customac/rolescan deny every statement butban— 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 legacyuser.role === 'admin'scalar ADR-0068 D2 stopped synthesizing.Q3 — can the server-side API be invoked without mounting the HTTP endpoints? Yes.
createBetterAuthbuildsapifromgetEndpoints(authContext, options)atdist/auth/base.mjs:9and returns it at:44; the router is constructed separately, only inside the request handler, at:38.disabledPathslives in the router'sonRequest. So with every/admin/*path indisabledPaths,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 callsgetAuthoritativeSessionFromCtxand throws UNAUTHORIZED without a session. Soauth.api.banUseris 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 inadmin-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:2801rawApp.all(basePath + '/*')), so they win:create-user:2237,ban-user:2306,unban-user:2327,remove-user:2399,has-permission:2452,set-user-password:2487; andimpersonate-useris 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 legacyrole='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'sschema(thebanned/banReason/banExpires/rolecolumns, mapped bybuildAdminPluginSchema()atauth-manager.ts:2969-2971) and itsinit()hooks (admin.mjs:25-52) — the session-create hook that refuses a banned user withBANNED_USERand auto-lifts expired bans.auth-plugin.ts:2288-2303already 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 whenauth.api.banUseris absent. Route C keeps schema andinit()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.latestpublished pinned / resolved here better-auth1.7.2 2026-08-26T19:06:23Z ^1.7.2/ 1.7.2@better-auth/core1.7.2 2026-08-26T19:13:25Z ^1.7.2/ 1.7.2@better-auth/scim1.7.2 2026-08-26T19:03:23Z ^1.7.2/ 1.7.2@better-auth/sso1.7.2 2026-08-26T19:03:23Z ^1.7.2/ 1.7.2@better-auth/oauth-provider1.7.2 2026-08-26T19:03:23Z ^1.7.2/ 1.7.2No newer stable exists; the other dist-tags are all older lines, not newer ones (
beta1.7.0-beta.10,rc1.7.0-rc.6,release-1.41.4.22,canary1.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/scimto 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.0release is where the admin/ban coupling left@better-auth/scim(the tarball table in Premise B). The upstream release notes forv1.7.0corroborate 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 against1.7.0-rc.1; against1.7.2the 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, SCIMactive: falsetoday 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.reconcileUserhost hook calling ObjectStack's own ban write — whichadmin-ban-endpoints.tsalready owns, complete with the last-admin guard. That still needs the admin plugin's schema andinit()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
adminunder SCIM; if a disable is wanted, implementidentity.reconcileUserover ObjectStack's own ban write", and give it Route C (narrow the plugin'sendpointsrecord) 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#1451is parked on this chain; [finding] SCIM forces the better-authadminplugin 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.mdxis toldadminis "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
reconcileUserhook, 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-authrelease notes via the GitHub API — HTTP 403 from the session proxy onhttps://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
disabledPathsand 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.adminexplicitly. Config-time fact, not visible in this repo.
Out-of-scope findings filed (unassigned, deduped first)
- [finding] SCIM active:false no longer disables the account — the vendor ban coupling was removed upstream in @better-auth/scim 1.7.0 and nothing in this repo replaced it #14360 — SCIM
active: falseno longer disables the account; the vendor ban coupling left@better-auth/scimin 1.7.0 and nothing here replaced it. Also names the unreachable plugin-auth: break-glass 守卫 —— SCIM/ban 不得停用最后一个管理员(ADR-0024 D5.2,cloud#621 转入) #5892 break-glass trigger. - [finding] The SCIM/identity ADR-0071 citation resolves to the dataset semantic-layer record — 39 files point at a decision about multi-hop joins #14361 — the SCIM/identity
ADR-0071citation resolves to the dataset semantic-layer record; 39 files, andcheck-adr-anchorscannot see it because the number does resolve.
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 for0071,deprovision,active: false,reconcileUser; and one targetedsearch_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
ACCEPT — measurement delivered as the
os-dev-reportcomment (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:
- Admin-plugin shape at the pinned version — plugin options cannot prune endpoints (15-endpoint literal,
admin.mjs:66-82; reverse control: all 11AdminOptionsmembers enumerated, none endpoint-related).banIS obtainable alone by two real routes: top-leveldisabledPaths(router-side, HTTP-only, fail-open denylist) and narrowing the plugin's ownendpointsrecord (fail-closed allowlist; this repo's landed technique atadmin-impersonate-endpoint.ts:198, keepsinit()/schema).auth.api.banUsersurvives either. - Upgrade evaluation — no upgrade exists:
dist-tags.latestis1.7.2for all five better-auth packages, exactly what is pinned and resolved. The upgrade contingency does not fire. - Fork — FEASIBLE on the current version with no upgrade.
premise_still_valid: falseon two premises, and that is a good run, not a failure: (B) the SCIM→ban coupling left@better-auth/scimat 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 theidentity.reconcileUserhost callback; (C)ADR-0071in 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):
- [finding] SCIM active:false no longer disables the account — the vendor ban coupling was removed upstream in @better-auth/scim 1.7.0 and nothing in this repo replaced it #14360 — limb A: implement
identity.reconcileUserover this platform's own ban write. Triage graded it Bug · p1 · security, dispatchable in this lane once this card closes; it is the precondition for any narrowing of the forced admin mount, because under limb A SCIM deactivation needs admin's schema + init hooks (thebannedcolumn and the sign-in refusal) but none of its endpoints. - [finding] The SCIM/identity ADR-0071 citation resolves to the dataset semantic-layer record — 39 files point at a decision about multi-hop joins #14361 — the ADR-0071 mis-citation: decision inbox with the four facets (an ADR-class question is the human floor).
- [finding] SCIM forces the better-auth
adminplugin on (pluginConfig.admin ?? scimEffective) and a deployment has no declared way to decline it — split from #13439 by the 2026-08-31 ruling #13816 — reading note posted: shape 2 is transitional; the shape-1 card is cut after [finding] SCIM active:false no longer disables the account — the vendor ban coupling was removed upstream in @better-auth/scim 1.7.0 and nothing in this repo replaced it #14360 lands, in the narrowed form limb A implies (schema + init, no endpoints), not the "drop everything" form.
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_issuescalls, 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 on8094834), andpm:dispatched/pm:blockingare stripped with the close.
Generated by Claude Code
- Admin-plugin shape at the pinned version — plugin options cannot prune endpoints (15-endpoint literal,
由 #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 插件的实际形状,没人测过。要交付的三个读数
auth.api.banUser类)是否可以在不 mount HTTP 端点的情况下调用?)以源码/类型为准,不以文档想象。adminplugin on (pluginConfig.admin ?? scimEffective) and a deployment has no declared way to decline it — split from #13439 by the 2026-08-31 ruling #13816 的形状 2 降为过渡态;adminplugin on (pluginConfig.admin ?? scimEffective) and a deployment has no declared way to decline it — split from #13439 by the 2026-08-31 ruling #13816 落地后本链关闭。Refs: #13816(裁决与形状 2 实施卡)· #13439(已裁姊妹卡:三键声明)· ADR-0071 · objectstack-ai/cloud#1451。