From 0451231e044a92d6e9fe3296e06061c1bc259328 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 19:52:40 +0000 Subject: [PATCH] docs(plugin-auth): correct the has-permission header's fail-direction claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01UQgPSniH1GFM9ZDeGyuGUa --- .../src/admin-has-permission-endpoint.ts | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts b/packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts index 8b3a6645b5..87663f2a03 100644 --- a/packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts +++ b/packages/plugins/plugin-auth/src/admin-has-permission-endpoint.ts @@ -52,13 +52,21 @@ * * ## Fail direction * - * Every uncertainty delegates: an unreadable body, a shape outside the set - * the vendor evaluates, an unreadable live-options object. Delegation can - * only reproduce the vendor's measured native behaviour — it can never mint - * a `true` for a caller the predicate did not admit. The only path to - * `success: true` runs through `isPlatformAdminUser` (or the vendor's own - * `adminUserIds` short-circuit, mirrored below for option fidelity; this - * repo configures none). + * Two of the three uncertainties delegate: an unreadable body and a shape + * outside the set the vendor evaluates. Delegation can only reproduce the + * vendor's measured native behaviour — it can never mint a `true` for a + * caller the predicate did not admit. The only path to `success: true` runs + * through `isPlatformAdminUser` (or the vendor's own `adminUserIds` + * short-circuit, mirrored below for option fidelity; this repo configures + * none). + * + * The third — an unreadable live-options object — does NOT delegate. It is + * caught in {@link answerPermissionQueryAsAdmin}, `adminOptions` becomes + * `undefined`, and the evaluation answers from the vendor's exported + * `defaultRoles` with `adminRoles = ['admin']`: on that path the + * deployment's own `roles` / `adminRoles` are not the ones read. It is + * reached only after `isPlatformAdminUser` has already admitted the caller, + * so it still cannot answer for a caller the predicate refused. * * ## Why the evaluated-body set is spelled out here * @@ -158,8 +166,10 @@ export async function answerPermissionQueryAsAdmin( (p) => p?.id === 'admin', )?.options; } catch { - // Unreadable live options → run on the vendor's own defaults, exactly as - // the vendor itself would with an unconfigured plugin. + // Unreadable live options → evaluate on the vendor's exported defaults + // (`defaultRoles`, `adminRoles = ['admin']`) instead of this deployment's + // configured ones. This path answers; it does not delegate (see the + // header's "Fail direction"). adminOptions = undefined; } const opts = (adminOptions ?? {}) as {