Uh oh!
There was an error while loading. Please reload this page.
docs(plugin-auth): correct the has-permission header's fail-direction claim - #12362
Conversation
… claim The `admin-has-permission-endpoint.ts` header's "Fail direction" paragraph enumerated three uncertainties and said every one of them delegates. Two do: an unreadable body and a shape outside the set the vendor evaluates both return `undefined` from `readEvaluatedPermissionQuery` and fall through to `AuthManager.handleRequest`. The third does not. An options read that throws is caught, `adminOptions` becomes `undefined`, and the evaluation continues on better-auth's exported `defaultRoles` with `adminRoles = ['admin']` — it answers rather than delegating, and on that path the deployment's own `roles` / `adminRoles` are not the ones read. On a file whose whole subject is which authority answers, that sentence reads as a security invariant and is not one. The correction states what the code does and claims nothing further; the neighbouring sentence — that delegation can never mint a `true` for a caller the predicate did not admit — is true and is kept verbatim, since the fallback is reached only after `isPlatformAdminUser` has already admitted the caller. The site comment at the `catch` keeps its mechanism half and loses its comparison: "exactly as the vendor itself would with an unconfigured plugin" does not hold, because the vendor never runs unconfigured here — it reads its own live options off its own plugin instance and cannot fail to. Comments only. Zero executable lines change; no `dist/` byte moves. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 28f244b32718d2639fe5823a5e5426f16b9426b2 && git checkout 28f244b32718d2639fe5823a5e5426f16b9426b2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a6c1e216fcbe39c15e0f31cff4d82275f38085f0 0451231e044a92d6e9fe3296e06061c1bc259328 && git checkout -B drift-repro a6c1e216fcbe39c15e0f31cff4d82275f38085f0 && git merge --no-ff 0451231e044a92d6e9fe3296e06061c1bc259328
node scripts/docs-audit/affected-docs.mjs --json a6c1e216fcbe39c15e0f31cff4d82275f38085f0 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12270
Comment-only correction in
packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts— zero executable lines change, nodist/byte moves. Scope is disposition (a) exactly as triage pinned it; (b) is untouched and remains a maintainer-floor call (see "The fork" below, which measures whether it is required — it is not, on the evidence in this tree).The two replaced passages, quoted so review need not fetch the diff twice
1. Header,
## Fail directionBefore
After
The middle sentence — delegation can never mint a
truefor a caller the predicate did not admit — is kept verbatim. It was and remains true: the fallback sits after the predicate has already admitted the caller. Only the enumeration around it moved.2. Site comment at the
catchinanswerPermissionQueryAsAdminBefore
After
Why each was wrong
readEvaluatedPermissionQueryreturnsundefinedand the mount falls through toAuthManager.handleRequest. The third does not: thecatchsetsadminOptions = undefinedand execution continues into the same evaluation, ondefaultRoleswithadminRoles = ['admin']. It answers. On a file whose whole subject is which authority answers, a header sentence that reads as a security invariant and is not one is the defect; the next reader to cite it would cite it as true.Neither replacement asserts anything the code does not do. In particular the new text does not claim the fallback is unreachable, does not claim it is safe, and does not claim a direction for how a configured deployment's answer would differ — only that the configured
roles/adminRolesare not what that path reads.The fork, measured rather than reasoned about
Whether (a) is sufficient turns on one question: is the fallback reachable in a supported deployment shape? Both conditions must hold.
Condition 1 — can
AuthManager.getAuthContext()throw? Plausibly yes. It isauth-manager.ts:4485:await this.getOrCreateAuth()then.$context, andgetOrCreateAuthcallscreateAuthInstance(), which dynamically importsbetter-auth, builds the plugin list and the database config. A boot failure there propagates.Condition 2 — does any deployment configure custom
roles/adminRoles?Zero hits, and there is no surface through which a deployment could.Every
admin(...)construction in the tree passes{ schema }and nothing else:packages/plugins/plugin-auth/src/auth-manager.ts:2830— the one production site:admin({ schema: buildAdminPluginSchema() })last-admin-guard.test.ts:548,879,better-auth-schema-parity.test.ts:227,credential-at-rest-posture.test.ts:471,managed-extension-fields.test.ts— all{ schema }onlyadmin()hits inpackages/runtime/src/domains/*.test.tsare a local test helper of the same name, not the better-auth pluginNo
roles:hit is a better-auth admin-plugin option (they are ObjectQL positions, spec schemas, a security-plugin fixture, and one test helper argument). And:There is no passthrough by which a deployment supplies its own better-auth plugin options. So condition 2 is not merely unmet today — it has no configuration surface in this repo, which is why this PR does not stop for a ruling: nothing here shows (b) is required. That remains a maintainer call on the merits, not a blocker this change creates. The fork is reported on the card.
Verification
Run against the tree now at
0451231e0(working tree clean at that commit, so every run below covers exactly HEAD's bytes).pnpm --filter @objectstack/plugin-auth typecheck— exit 0, captured before any pipe. Needs the package's owndist/for its second program (tsconfig.examples.json), so the dependency closure and the package were built first:pnpm --filter '@objectstack/plugin-auth^...' buildandpnpm --filter @objectstack/plugin-auth build, both exit 0.pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2— Test Files 80 passed (80) · Tests 1631 passed (1631).admin-has-permission-endpoint.test.tson its own: 13 passed (13).node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(11 families for this path), each exit code captured before any pipe — all exit 0. Their own verdict lines include:check-nul-bytes: OK (scanned 6834 text file(s) -- 6834 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).OK: 17 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-type-source-resolution OK — 93 tsc program(s) across 77 packages scannedcheck-test-source-alias OK — 72 packages with tests scannedcheck:page-declaration-shape,check:published-files,check:slot-lookup,check-ci-filter-parity,check-cross-package-test-inputs,check-plugin-teardown-shape,check-affected-docs,check-drift-comment.git diff -U0 | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-]\s*(\*|//|/\*\*)'returns empty — every changed line is a comment line.pnpm lintis CI's run. Locally the changed file was linted directly: 1 file, 0 errors, 0 warnings (count read from--format json, not estimated), the file present in ESLint's own resolved population with no ignore warning. The narrowing excludes nothing, becauseeslint.config.mjsstates in its own text that this repo "runs oneeslint.config.mjs, which never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file, test or not" — with no cross-file type program, a comment edit in one file cannot move any untouched file's verdict.Changeset
None, on purpose. Comments do not ship —
tsupstrips them and nodist/byte moves — so theskip-changesetlabel carries the declaration rather than an empty changeset, which stalls the release here. IfCheck Changesetdisagrees, that is reported rather than answered with an invented bump.Contract review
Not applicable. Comment text changes no accept/reject behaviour and widens no surface, so
needs:contract-reviewis deliberately not hung here.Refs: #11900 / PR #12210 (where this code landed and where the finding was raised) · ADR-0068 D2 (the predicate) · #12029 (the gate-then-delegate seam this module inverts). None of those is closed by this PR.
Generated by Claude Code