You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: split role-gating into two topologies (Bearer/API edge gate vs cookie-session SSR {from}) (#423)
D1 of add-ssr-role-guard-helper. Documents that the deploy-spec requireRole edge gate authenticates via Bearer JWT only (doesn't see cookie sessions), is per-function, and returns a JSON 403; cookie-session SSR (Astro/Next) uses the in-function auth.requireRole/role({from}) guard (@run402/functions 3.5.0). Astro SSR catch-all as the worked example.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -411,7 +411,7 @@ Rules and footnotes:
411
411
-**Deploy-time validation.** Missing table or column at activation fails with `DEPLOY_INVALID_ROLE_GATE` (422) *before* flipping the live release.
412
412
-**Cache TTL.** Default 60s, max 600s. A demoted user keeps the cached role until expiry — for instant revocation, set `cacheTtl: 0` (fresh lookup per request).
413
413
-**Gate applies to both** routed (`/your/route`) and direct (`POST /functions/v1/:name` with API key) invocation. Direct invocation still requires the API key at the edge; the gate runs after API-key auth, against the user JWT.
414
-
-**Reading the role (`@run402/functions` 3.4.0+).**`await auth.requireRole("operator")` returns `{ user, role }`; it throws a distinct `RoleGateNotConfiguredError` (500) when no `requireRole`gate is declared (vs `InsufficientRoleError` 403 for a real mismatch). For multi-role gates, `await auth.role()`returns the resolved role (or `null`, never throws)so you branch instead of re-asserting.
414
+
-**Reading the role — TWO topologies (`@run402/functions` 3.4.0+; `{ from }` since 3.5.0).**(1) **Bearer/API → edge gate:** with a `requireRole` gate declared, `await auth.requireRole("operator")` returns `{ user, role }`, throwing `RoleGateNotConfiguredError` (500) if no gate vs `InsufficientRoleError`(403) for a real mismatch; for multi-role gates read `await auth.role()`(role or `null`, never throws). The edge gate authenticates via `Authorization: Bearer` ONLY — it does NOT see the browser cookie session, and is per-function. (2) **Cookie-session SSR (Astro/Next) → in-function `{ from }`:** the edge gate doesn't apply (no Bearer; can't scope a catch-all to `/admin/*`; JSON 403 not a redirect), so pass `{ from: { table, idColumn, roleColumn } }` — the helper resolves the cookie user + reads their role from your tenant table directly (RLS-bypass), no gate. On `.astro` pages use `await auth.role({ from })` + `Astro.redirect("/admin/login", 303)` (a throw in frontmatter renders a 500).
415
415
-**Scaffold + first-operator bootstrap.**`run402 auth scaffold-roles --roles operator` emits the `app_roles` migration, the `requireRole` snippet, and a service-role `INSERT` for the FIRST operator — the table starts empty, so the first grant bypasses RLS with the service key. The gate keys on the tenant user id (JWT `sub`), not a wallet.
Copy file name to clipboardExpand all lines: cli/llms-cli.txt
+14-1Lines changed: 14 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -639,7 +639,20 @@ Validation rules:
639
639
- Empty `allowed`. Rejected with `INVALID_SPEC` — set the role values explicitly.
640
640
- Deploy-time validation. Missing table or column at activation fails with `DEPLOY_INVALID_ROLE_GATE` (HTTP 422) *before* flipping the live release. `run402 deploy apply` surfaces the structured envelope on stderr.
641
641
642
-
Reading the role in-function (`@run402/functions` 3.4.0+). `await auth.requireRole("operator")` returns `{ user, role }` or throws — and throws a *distinct* `RoleGateNotConfiguredError` (server-class 500) when the function declares no `requireRole` gate, vs `InsufficientRoleError` (403) for a real role mismatch. When a gate allows more than one role, read `await auth.role()` (returns the resolved role or `null`, never throws) and branch instead of re-asserting — `requireRole(x)` requires `x` to be one of `allowed`.
642
+
Reading the role in-function — TWO topologies (`@run402/functions` 3.4.0+; `{ from }` since 3.5.0). Role gating has two non-interchangeable forms; pick by how the caller authenticates.
643
+
644
+
1. Bearer/API callers → the deploy-spec `requireRole` EDGE gate (above). With a gate declared, `await auth.requireRole("operator")` returns `{ user, role }`, throwing a *distinct* `RoleGateNotConfiguredError` (server-class 500) if no gate is declared vs `InsufficientRoleError` (403) for a real mismatch. For multi-role gates, read `await auth.role()` (resolved role or `null`, never throws) and branch — `requireRole(x)` requires `x` in `allowed`. IMPORTANT: the edge gate authenticates via `Authorization: Bearer` JWT ONLY — it does NOT see the browser cookie session, and it is per-function.
645
+
646
+
2. Cookie-session SSR (Astro/Next consoles, dashboards) → the in-function `{ from }` guard. The edge gate does not apply here (no Bearer → it 401s every cookie browser; per-function → one catch-all function can't gate just `/admin/*`; it returns a JSON 403, not a redirect). Pass `{ from }` and the helper is self-contained: it resolves the cookie user (`auth.user()`) and reads their role from your tenant table directly (RLS-bypass), no gate required:
if (role !== "operator") return Astro.redirect("/admin/login", 303);
653
+
```
654
+
655
+
Worked example — an Astro SSR hybrid where ONE ssr function serves `/admin/*` AND the public catch-all/404 fallback: use the in-function `{ from }` guard. An edge gate there would also gate public 404s + the public `/admin/login`, and would 401 every cookie browser. `{ from }` scopes the check to your admin pages, in-app, where the cookie identity is available.
643
656
644
657
Scaffolding + first-operator bootstrap. `run402 auth scaffold-roles --roles operator` emits the conventional `app_roles(user_id uuid, role text)` migration, the matching `requireRole` snippet, and a service-role `INSERT` to grant the FIRST role — the table starts empty, so the first grant must bypass RLS with the service key. The gate keys on the tenant user id (`internal.users.id` / JWT `sub`), NOT a wallet. The conventional table is a default — `requireRole` accepts any `(table, idColumn, roleColumn)`.
0 commit comments