Skip to content

security(plugin-auth): gate direct /sso/register on platform admin only - #10390

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-10009-sso-register-platform-admin-only
Aug 21, 2026
Merged

security(plugin-auth): gate direct /sso/register on platform admin only#10390
os-warren merged 4 commits into
mainfrom
claude/issue-10009-sso-register-platform-admin-only

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10009

⚠️ This narrows an auth accept-set. Who loses access, first:

An organization owner or admin who is not a platform admin can no longer register an SSO identity provider. They previously passed the ADR-0024 before-hook on the direct POST /api/v1/auth/sso/register and now receive 403 SSO_REGISTER_FORBIDDEN.

PrincipalBeforeAfter
Org owner/admin, no platform grantadmitted403 SSO_REGISTER_FORBIDDEN
Platform admin (org-less admin_full_access)admittedadmitted (unchanged)
Anonymous401 (vendor sessionMiddleware)401 (unchanged)

No other surface changes: the /admin/sso/* bridges already refused this principal since #9653, and every in-repo product path reaches SSO registration through those bridges (measured below).

Not to be auto-merged. Clause-② (accept-set narrowing on an auth surface) — developed at claude-opus-5 under the maintainer's 2026-08-20 authorization while fable is exhausted, and waiting on triage review before landing.

The ruling

Maintainer ruling 2026-08-20 (issue comment 5353918178) chose option (a): tighten the direct hook to platform-admin-only, aligning it with the #9653 posture on the bridges. ADR-0068 D4 — registering an identity provider is a platform-operator action. Options (b) (split by provider scope) and (c) (document the divergence) were rejected; if multi-org IdP self-serve ever becomes a product goal, (b) is a deliberate future ruling, not something inherited from the vendor default.

Premise, re-established before changing anything

Measured on one tree, one run, on this branch before the fix — the same org-owner principal at both doors:

PREMISE member role = "owner" (a real org owner)
PREMISE legacy sys_user.role = null (NOT riding the retired 'admin' scalar)
PREMISE permission-set grants = [] (NOT a platform admin)
PREMISE ① DIRECT /sso/register → 422 {"message":"This providerId is reserved …"} ← ADMITTED
PREMISE ② BRIDGE /admin/sso/register → 403 {"error":{"code":"PERMISSION_DENIED", …}} ← REFUSED

providerId: 'credential' is permanently reserved by @better-auth/sso, and the reserved-id refusal sits after the vendor's whole authorization prologue and before any discovery fetch — so 422 /reserved/ proves admission with no network. The divergence #10009 describes is real.

The shared judge did not fit this call site — why, rather than a fourth copy

platform-admin-gate.ts is the shared judge, and its own header argues that N copies of an authorization predicate is the shape that drifts. But it answers the session-shaped question: isPlatformAdminUser(sessionUser) reads isPlatformAdmin / positions[] / the legacy role scalar — fields customSession contributes.

This call site holds no session user. It resolves its actor through resolveActor(ctx), the shared hook-order-independent resolver used by ~5 gates, which returns { userId, activeOrgId } — an id. Feeding the session-shaped judge would need either a second session resolution (the exact "two resolution sites" defect auth-manager.ts's own #8102 comment says never to reintroduce) or a change to that shared contract. Worse, a raw session user carries no positions[], so isPlatformAdminUser would fall through to role === 'admin' — the retired D2 channel — refusing real permission-set platform admins and admitting only the channel this family is closing.

So this needs the id-shaped judge, which #10348 records as not yet consolidated. Rather than mint a fourth, differently-named spelling, this PR uses the exact name and body #10352 is already landing (isPlatformAdminUserId), so the two collide loudly on one identifier and the merge resolution is "keep one". #10348's other spellings are deliberately untouched here.

The wider predicate is removed, not parked

With /sso/register no longer asking the org question, isOrgOrPlatformAdmin lost its only production caller — and the compiler said so: error TS6133: 'isOrgOrPlatformAdmin' is declared but its value is never read (noUnusedLocals, tests excluded from the package tsconfig). It is removed rather than kept dead. Its isOrgAdminGrade import went with it.

#5942's substance is not lost coverage: the one grade ladder keeps its own direct pins in member-role-canonical.test.ts (case, comma and array spellings) and its other reader in last-admin-guard.ts. What is deliberately gone is the claim that this seam asks the org question — it no longer does, and the new block pins that as a refusal rather than leaving it unstated.

Pins — two-directional on purpose

The hook had no pins at all before this: SSO_REGISTER_FORBIDDEN appeared nowhere outside its own throw. A refusal-only suite would pass on a hook that refuses everyone, so both directions are asserted:

  • ① org owner → 403andSSO_REGISTER_FORBIDDEN (ADR-0112: code AND status, never one alone)
  • ② platform admin → still admitted (reaches the vendor's 422 /reserved/)
  • ③ anonymous → still 401, the vendor's own
  • ④ one principal, both doors, one answer

The platform admin is granted the ADR-0068 way — an org-less sys_user_permission_set link to admin_full_access — and the fixture asserts sys_user.role is NOT 'admin', so the suite cannot pass by riding the retired scalar.

Everything is real: a real ObjectQL engine over real better-sqlite3, the real AuthManager with the real sso() plugin, real sign-up and real session cookies. Only sys_permission_set / sys_user_permission_set are declared locally (they live in @objectstack/plugin-security) — the last-admin-guard.test.ts precedent, so a fixture adds no dependency edge.

Ablation — predicted signature stated first, both directions

AuthManager is imported relatively (./auth-manager.js) and there is no vitest alias, so vitest executes the mutated source; the dist/ staleness hazard does not apply to the mutated module, and no rebuild could change what these legs measured.

Leggit hash-objectPredictedObserved
pre-ablationda7f6561…240/240 green
A — narrowing absent (judge admits all)7e29fb56…refusal pins RED, admission pins GREEN13 failed: expected 422 to be 403 ×2, expected true to be false ×11. Pins ② and ③ stayed green
B — gate refuses everyone (judge always false)35112457…admission pins RED, every refusal pin GREEN3 failed: expected 403 to be 422, expected false to be true ×2. Every refusal pin stayed green
restoreda7f6561… — byte-identical to pre-ablationgreen again240/240 green, no ABLATION marker in the file

Leg B is the direct demonstration that a one-directional suite would have been blind here: under a hook that refuses everyone, every refusal assertion stays green and only the admission pins catch it.

Verification

Gate union run after the final commit on a clean worktree, node scripts/pm/dispatch-gates.mjs with no paths passed, at be985a623. Exit codes captured before any pipe; verdicts quoted from each gate's own line.

EXIT=0 pnpm check:changeset-gate-self-tests EXIT=0 node scripts/check-adr-0087-registration.mjs
EXIT=0 pnpm check:objectui-changeset EXIT=0 node scripts/check-changeset-no-major.mjs
EXIT=0 pnpm check:slot-lookup EXIT=0 node scripts/check-empty-changeset.mjs
EXIT=0 pnpm check:test-source-alias EXIT=0 node scripts/docs-audit/check-affected-docs.mjs
EXIT=0 pnpm check:type-source-resolution

Convention-triggered by the new/edited test files:

  • ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) … none new
  • check-engine-double-contract: OK — 338 pinned, 133 in the DEBT ledger, 2 exempt.
  • ✓ where-matcher conformance holds: 265 matcher(s) discovered … 0 silently-wrong
  • check-type-check-coverage: OK — 64/77 workspace packages type-checked
  • check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured … none above its recorded number.

check:type-check-debt first refused with a prerequisite, not a verdict (--re-measure cannot run: 30 workspace dependenc(ies) … have no built type entry point on disk). Satisfied it exactly as lint.yml does — turbo run build --filter='./packages/*' --filter='./packages/*/*', 70 successful, 70 total — and re-ran to the green above.

No ledger entry was raised.@objectstack/plugin-auth TEST_DEBT is left at 109 as instructed; the gate notes it now measures 108 (-1) and says lowering is optional (Not an error). Flagging rather than editing.

Package verification, on the merged tree: pnpm --filter @objectstack/plugin-auth typecheck clean, vitest run61 files / 1323 tests passed.

In-repo consumer population — bounded

Every search validated against a known-present control term first.

ScopeControlDirect /sso/register callers
objectstack (this repo)admin/sso/register → 9 files0 — all hits are prose/ADR text or the bridge target /api/v1/auth/admin/sso/register, including both sys_sso_provider metadata actions
objectui (console)api/v1/auth → present0
objectos (docs)api/v1/auth → present0
examples/, SDK, apps/signIn → present0

Zero in-repo callers of the direct endpoint; deployments are not visible from here. This is the half of triage's recorded confidence gap that is measurable from the repo — it does not establish that no deployed org admin uses the endpoint.

Serial note

PR #10352 (#9968) is open and touches the same file.auth-manager.ts is not on check-single-claim-paths.mjs's explicit path list, so the single-writer gate does not block either side; the residual risk is a merge conflict. origin/main was merged into this branch before the final commit (#10352 had not landed). The overlap is deliberate and named: both branches add a private isPlatformAdminUserId with the same body — keep one. #10352 additionally references {@link isOrgOrPlatformAdmin}, which this PR removes, so that docblock line needs updating on whichever side lands second.

Out of scope, filed separately

See the linked findings in the report; nothing outside this hook's region was changed. packages/spec/** was not touchedSSO_REGISTER_FORBIDDEN is already registered in error-code-ledger.zod.ts, so the refusal reuses the existing catalog member and no error-code registration was needed.


Generated by Claude Code

The ADR-0024 before-hook on better-auth's own POST /sso/register admitted a
platform admin OR an owner/admin of the caller's active org, while the four
/admin/sso/* bridges have gated on the platform-admin judge since #9653. One
principal therefore got two answers for the same underlying registration --
403 at the bridge, admitted at the direct endpoint -- which made the bridge
tightening honest labelling rather than a boundary.
Per the 2026-08-20 maintainer ruling on #10009, ADR-0068 D4 governs:
registering an identity provider is a platform-operator action. The admit set
here is now platform-admin-only.
- `isPlatformAdminUserId` asks the ADR-0068 D2 permission-set question on its
own (org-less admin_full_access link), through `withSystemReadContext`, fail
closed. It deliberately does not consult the retired `role` scalar.
- `isOrgOrPlatformAdmin` lost its only production caller with this change and
is removed rather than parked; the one grade ladder it delegated to
(`isOrgAdminGrade`) keeps its own pins in member-role-canonical.test.ts and
its other reader in last-admin-guard.ts.
- The direct surface gains its first test pins, two-directional on purpose:
the org owner is refused (403 + SSO_REGISTER_FORBIDDEN, code AND status) and
the platform admin is still admitted. The platform admin is granted the
ADR-0068 way and the fixture asserts the legacy role scalar is NOT 'admin',
so the suite cannot pass by riding the retired channel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

github-actionsBot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 7 documentable anchor(s).

14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx(via sys_permission_set (literal))
  • content/docs/automation/approvals.mdx(via admin_full_access (literal))
  • content/docs/data-modeling/objects.mdx(via sys_user_permission_set (literal))
  • content/docs/deployment/tenancy-modes.mdx(via sys_member (literal))
  • content/docs/kernel/contracts/auth-service.mdx(via AuthManager (symbol))
  • content/docs/kernel/services-checklist.mdx(via AuthManager (symbol))
  • content/docs/permissions/authentication.mdx(via AuthManager (symbol), sys_member (literal))
  • content/docs/permissions/authorization.mdx(via admin_full_access (literal), sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/delegated-administration.mdx(via sys_member (literal), sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/permission-sets.mdx(via admin_full_access (literal), sys_member (literal), sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/permissions/permissions-matrix.mdx(via admin_full_access (literal))
  • content/docs/permissions/positions.mdx(via sys_member (literal))
  • content/docs/permissions/sharing-rules.mdx(via admin_full_access (literal))
  • content/docs/ui/audience-based-interfaces.mdx(via admin_full_access (literal))

7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx(via sys_member (literal), sys_user_permission_set (literal))
  • content/docs/releases/v12.mdx(via sys_permission_set (literal))
  • content/docs/releases/v13.mdx(via sys_permission_set (literal), sys_user_permission_set (literal))
  • content/docs/releases/v14.mdx(via admin_full_access (literal), sys_user_permission_set (literal))
  • content/docs/releases/v15.mdx(via sys_permission_set (literal))
  • content/docs/releases/v16.mdx(via admin_full_access (literal), sys_member (literal), sys_user_permission_set (literal))
  • content/docs/releases/v17.mdx(via admin_full_access (literal), sys_member (literal), sys_permission_set (literal), sys_user_permission_set (literal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/last-admin-guard.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json dd8172ee223e14e3104191356fff10aa3f8abe33packageMentionDocs.

Which tree this was computed on

This run read content/docs from f51c802c0127fa3147079122ff4b30a5733ffae7 — the merge of head c231374d9d003ebab37c101b514b920e704e8a2e into base dd8172ee223e14e3104191356fff10aa3f8abe33, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f51c802c0127fa3147079122ff4b30a5733ffae7 && git checkout f51c802c0127fa3147079122ff4b30a5733ffae7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dd8172ee223e14e3104191356fff10aa3f8abe33 c231374d9d003ebab37c101b514b920e704e8a2e && git checkout -B drift-repro dd8172ee223e14e3104191356fff10aa3f8abe33 && git merge --no-ff c231374d9d003ebab37c101b514b920e704e8a2e
node scripts/docs-audit/affected-docs.mjs --json dd8172ee223e14e3104191356fff10aa3f8abe33

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dd8172ee223e14e3104191356fff10aa3f8abe33 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

⛔ 合并前置条件(PM 记录,非观察项)

本 PR 已通过 PM 内容验收(见 #10009),但保持 draft、不武装 auto-merge —— Clause-② auth 接受集收窄,按维护者 2026-08-20 授权走「opus 开发 + 分诊审核」。

我把 PR 正文里的 "Serial note" 从注记升格为放行条件。本车道有过一次教训:一条被记成「残留而非返工」的余量说明,一小时内爆掉并把仓库合并队列堵了 3.5 小时。所以这条写成条件。

条件本身

#10352#9968 的 impersonate 半边)与本 PR 都在 auth-manager.ts 的相邻区域新增 private async isPlatformAdminUserId(userId)方法体逐行一致。我核对过 #10352 的真实 diff,不是转述本 PR 的自述。

后落地的一侧会落到两种结果之一,两种都很响:

  • 文本冲突(两处改动区域相邻/重叠),或
  • git 三方合并自动接上 → 类里出现两个同名私有方法TS2393: Duplicate function implementationTypeScript Type Check 变红。

另有一处不会变红、因而必须靠这条记录兜住的:#10352 的 docblock 写着 "Deliberately NARROWER than {@link isOrgOrPlatformAdmin}",而本 PR 删除了 isOrgOrPlatformAdmin。两者都落地后那是一个悬空 {@link} —— TSDoc 引用,编译器不管,CI 不会红。

指定顺序

#10352,后 #10390

理由不是偏好:这个顺序下,两件事在同一次冲突解决里一起收掉 —— 删掉重复的 isPlatformAdminUserId(保留一份),同时把那行悬空 {@link} 改掉。反过来的顺序会让悬空链接由 #10352 一侧引入,而那一侧的作者没有理由知道它悬空。

⚠️ 合并本 PR 前请确认:如果 #10352 已落地,本分支必须先合 main人工解决为「保留一份 isPlatformAdminUserId + 修正 docblock 引用」,再跑一次绿,而不是依赖 auto-merge。


Generated by Claude Code

…o-register-platform-admin-only
# Conflicts:
#	packages/plugins/plugin-auth/src/auth-manager.ts
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Merge reconciliation with #10352 — measured, at c231374d9

origin/main merged into the branch (merge only — no rebase, no amend, no force-push). The Verification section in the PR body above still cites the pre-merge sha be985a623; this comment supersedes it. Base is now 72d75ebb5.

The two identifiers, counted before and after

Counts are grep -c in packages/plugins/plugin-auth/src/auth-manager.ts:

identifierbranch be985a623origin/mainmerged c231374d9
isPlatformAdminUserId223
isOrgOrPlatformAdmin131
isOrgAdminGrade030

isPlatformAdminUserId = 3 occurrences but exactly ONE definitiongrep -c 'private async isPlatformAdminUserId' = 1 (line 4387). The other two are call sites: line 1552 (this PR's /sso/register hook) and line 2625 (#10352's /admin/impersonate-user rewire). Both sides declared byte-identical bodies; one was dropped.

The surviving docblock is this branch's, which is why the {@link} is gone rather than stripped — the branch had already authored the replacement sentence:

Deliberately does NOT admit organization owners/admins. Platform-admin routes must not be reachable by whoever happens to own an org (ADR-0068). [#10009] This replaced an isOrgOrPlatformAdmin predicate that admitted both; once /sso/register stopped asking the org question, that wider predicate had no caller left and was removed rather than parked.

#10352's competing sentence ("Deliberately NARROWER than {@link isOrgOrPlatformAdmin}") is the one that lost, so no dangling link survives.

{@link} proof, with a positive control

grep -rn "{@link isOrgOrPlatformAdmin" src/ -> exit 1, ZERO hits
grep -rn "isPlatformAdminUserId" src/ -> exit 0, 6 hits (control: search works)

isOrgOrPlatformAdmin still appears 3 times in the package, all of it proselast-admin-guard.ts:96, auth-manager.ts:4375, auth-manager.test.ts:3946. Each is a backticked historical mention that explicitly says the predicate is gone; none is a {@link} and none is a code reference. Deliberately left standing.

isOrgAdminGrade

Its auth-manager.ts import stayed dropped: on main all 3 occurrences lived inside isOrgOrPlatformAdmin's own docblock and body, so removing that method left no reader in this file. #10352 does not read it. The symbol is alive elsewhere in the package (invitation-role-cap.ts exports it, last-admin-guard.ts:281,827 imports and calls it, member-role-canonical.test.ts pins it) — unaffected.

Package verification on the merged tree

EXIT=0 pnpm --filter @objectstack/plugin-auth typecheck (tsc --noEmit)
EXIT=0 pnpm --filter @objectstack/plugin-auth test 62 files / 1347 tests passed

61/1323 → 62/1347: #10352 added admin-impersonate-endpoint.test.ts and modified four more plugin-auth test files. Delta accounted for, not assumed.

Gates — union derived by node scripts/pm/dispatch-gates.mjs with no path arguments, after the final commit, clean tree

EXIT=0 pnpm check:changeset-gate-self-tests EXIT=0 node scripts/check-adr-0087-registration.mjs
EXIT=0 pnpm check:objectui-changeset EXIT=0 node scripts/check-changeset-no-major.mjs
EXIT=0 pnpm check:slot-lookup EXIT=0 node scripts/check-empty-changeset.mjs
EXIT=0 pnpm check:test-source-alias EXIT=0 node scripts/docs-audit/check-affected-docs.mjs
EXIT=0 pnpm check:type-source-resolution
EXIT=0 pnpm check:query-options-erasure EXIT=0 pnpm check:engine-double-contract
EXIT=0 pnpm check:where-matcher EXIT=0 pnpm check:type-check-coverage

Class #10309 confirmed: the derivation did NOT name check:route-envelope or check:dispatcher-error-vocabulary. Both bite plugin-auth and #10352 moved both populations. Run explicitly:

EXIT=0 pnpm check:route-envelope EXIT=0 pnpm check:route-envelope --self-test
EXIT=0 pnpm check:dispatcher-error-vocabulary EXIT=0 pnpm check:dispatcher-error-vocabulary --self-test
  • ✓ Plugin-mounted Hono routes — 12 module(s) audited, 166 hand-built body/bodies … 8 conformant, 0 ratcheted, 3 exempt, 1 vendor-wire
  • check-dispatcher-error-vocabulary: OK — 21 unregistered code-stamping site(s), all classified; 1 awaiting a ledger entry (#8846).
  • check-engine-double-contract: OK — 371 pinned, 133 in the DEBT ledger, 2 exempt. (was 338 pre-merge; main moved it)
  • ✓ where-matcher conformance holds: 271 matcher(s) discovered, 271 answer the combinator battery correctly or refuse it loudly (162 refuse).
  • check-type-check-coverage: OK — 64/77 workspace packages type-checked … 13 in the DEBT ledger, 1 exempt.

Declared narrowing — check:type-check-debt --re-measure was NOT run

It needs the whole workspace closure built, and that build runs under the container-shared verification lock, which was held continuously by sibling agents (queue 3 deep). Attempted anyway: it did not refuse on the prerequisite this time, it entered the per-ledger-entry tsc sweep and was still running at a 420s cap, so it is NOT MEASURED — not a pass, not a fail. In its place tsc --noEmit for this package is clean at exit 0. CI runs the ratchet regardless.

@objectstack/plugin-auth TEST_DEBT is untouched at 109 (card #10615). No --lower was run and no ledger number was edited.

Still draft; auto-merge not armed; nothing merged.


Generated by Claude Code


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review August 21, 2026 08:59
@os-warren
os-warren enabled auto-merge August 21, 2026 08:59
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 授权:翻 ready + 武装 auto-merge(2026-08-21T08:59Z)

Clause-② 的暂扣已按流程解除,不是我自己解的:分诊席(非派发席)于 2026-08-21T00:40:55Z#10009 上贴出 contract review PASS 并清掉 needs:contract-review(comment 5363784866)。补偿控制走完了完整路径 —— opus 开发 → draft 保持 → 分诊按内容评审 → 清标 → 现在才武装队列。⛔ PM 全程没自清标、没自评审、没绕合并队列。

收尾按内容核对(不采信报告自述)

c231374d9 上我自己量的:

读数
历史形态be985a623仍是新 head 的祖先 ⇒ 没有 rebase / amend / force-push,是合并提交
private async isPlatformAdminUserId1auth-manager.ts:4387)—— keep-one 达成
调用点2 —— :1552(本 PR 的 /sso/register 闸门)、:2625#10352/admin/impersonate-user 改接)⇒ 两侧各自需要的都在
{@link isOrgOrPlatformAdmin in src/0(exit 1)。阳性对照:同一棵 src 树 {@link 大量命中 ⇒ 搜索有效
isOrgAdminGrade仍活:invitation-role-cap.ts:109 导出,last-admin-guard.ts:281/827 导入并调用,member-role-canonical.test.ts 有直接 pin ⇒ #5942 的实质没丢

那条悬空 {@link} 是本次收尾唯一编译器和门禁都抓不到的危险(noUnusedLocals/TS6133 只抓代码引用),所以它是零这件事必须用带对照的 grep 证明,而不是靠「改完了」的自述。

两条我写错、被 dev 当场推翻的前提 —— 记录在案

  1. 我要求「isOrgOrPlatformAdmin 在包内出现次」。这条指令是错的。 包内还剩 3 处散文提及(last-admin-guard.ts:96auth-manager.ts:4375auth-manager.test.ts:3946),每处都在明说该谓词已被移除;没有一处是 {@link},没有一处是代码引用。清到零等于删掉经评审的刻意解释。正确判据是零代码引用 + 零 {@link},而那个确实是零。采纳 dev 的判定。
  2. 分支 head 上 isOrgOrPlatformAdmin 本就是 ×1(一处反引号散文)而非 ×0,所以合并后的 ×1 是预期值,不是残留。

一条显式声明的收窄

check:type-check-debt --re-measure没有测到 —— 不是它拒跑,是进入逐条 tsc 扫描后在 420s 上限时仍在运行(全工作区构建锁被兄弟 agent 连排 3 深占用,其间一次 9 分钟队列超时 exit 99)。既不是 pass 也不是 fail。 替代物:pnpm --filter @objectstack/plugin-auth typechecktsc --noEmit)EXIT=0。CI 的 Type Check · debt ledger 无条件跑这道棘轮,由它给出真读数 —— 若它红,auto-merge 不会触发,这正是武装队列而非直接合并的意义。

@objectstack/plugin-auth TEST_DEBT 保持 109#10615)未动,没跑 --lower

class #10309 —— 第六次坐实

node scripts/pm/dispatch-gates.mjs(不带路径参数)推导出的并集再次没有点名check:route-envelopecheck:dispatcher-error-vocabulary,而两者都咬 plugin-auth。四次调用(含 --self-test)全部 EXIT=0,全部是靠派发里手写补进去才跑上的。

⚠️ 一条反向读数,留给 devx 车道而非本卡:#10449 的 ESLint 爆栈本地没复现 —— 该门禁自身的 self-test 打印 packages/spec/src/migrations/registry.ts parses at --stack-size=4000 through this gate's own channel。与 CI 上记录的现象不一致。

正文的陈旧处,已用评论更正而非重写

PR 正文 Verification 段仍引用合并前的 be985a623 与 61/1323 计数;合并后实测 62 files / 1347 tests passed(差额由 #10352 新增的 admin-impersonate-endpoint.test.ts 及其修改的四个测试文件完整解释)。dev 选择不重写正文(10KB,且已带 sanitizer 编码实体,往返有损坏已评审文本的风险),改为把更正记录在 PR 评论 5367733374我认可这个取舍 —— 更正的可追溯性优先于正文的整洁。


Generated by Claude Code

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

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-warren@claude