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: edge gate now serves cookie-session SSR (ssr-aware-role-gate A+C) (#426)
Updates the role-gating topology docs now that the edge gate authenticates cookie sessions (A) and supports onDeny:redirect (C). The discriminator shifts from 'how you authenticate' to function topology: dedicated function/route → edge gate (cookie-aware, onDeny:redirect, cached); catch-all SSR function or finer per-path control → in-function {from}. Completes task 4.1 of add-ssr-aware-role-gate.
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 — 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).
414
+
- **Reading the role — TWO approaches (`@run402/functions` 3.4.0+; `{ from }` since 3.5.0).** The edge gate now authenticates BOTH Bearer and cookie-session SSR callers (`ssr-aware-role-gate`), so pick by function topology. (1) **Dedicated function/route → edge gate:** with a `requireRole` gate, `await auth.requireRole("operator")` returns `{ user, role }` (throwing `RoleGateNotConfiguredError` 500 if no gate vs `InsufficientRoleError` 403 for a mismatch); for multi-role gates read `await auth.role()`. It authenticates Bearer AND cookie session, enforces before dispatch, and caches (TTL). For a browser console add `onDeny: "redirect"` + `signInPath` (same-origin path) → unauthenticated HTML requests get a `303` to sign-in (401-class only; wrong-role 403 stays an envelope). PER-FUNCTION. (2) **Catch-all SSR function (one fn = console + public fallback), or finer per-path control → in-function `{ from }`:** the per-function edge gate would also gate public 404s + `/admin/login`, so pass `{ from: { table, idColumn, roleColumn } }` — resolves the cookie user + reads their role from your tenant table (RLS-bypass), scoped in-app. 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
+4-4Lines changed: 4 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -639,11 +639,11 @@ 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 — 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.
642
+
Reading the role in-function — TWO approaches (`@run402/functions` 3.4.0+; `{ from }` since 3.5.0). The edge gate now authenticates BOTH `Authorization: Bearer` callers and cookie-session SSR browsers (capability `ssr-aware-role-gate`), so pick by your function TOPOLOGY, not by how the caller authenticates.
643
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.
644
+
1. Dedicated function / route → the deploy-spec `requireRole` EDGE gate (above). It authenticates Bearer JWT AND the browser cookie session, enforces before dispatch, and caches the role lookup (TTL). In-function, `await auth.requireRole("operator")` returns `{ user, role }` (throwing a *distinct* `RoleGateNotConfiguredError` (500) if no gate is declared vs `InsufficientRoleError` (403) for a mismatch); for multi-role gates read `await auth.role()` and branch — `requireRole(x)` requires `x` in `allowed`. For a browser console, add `onDeny: "redirect"` + `signInPath` (a same-origin path) to the gate so an unauthenticated HTML request gets a `303` to sign-in (401-class only — an authenticated wrong-role 403 stays a JSON envelope, no redirect loop). The gate is PER-FUNCTION.
645
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:
646
+
2. Catch-all SSR function, or finer per-path control → the in-function `{ from }` guard. Because the edge gate is per-function, a single catch-all function (one ssr function = `/admin/*` console AND the public 404 fallback) can't gate just `/admin/*` — an edge gate would also gate the public 404s and the public `/admin/login`. Use `{ from }`: it resolves the cookie user (`auth.user()`) and reads their role from your tenant table (RLS-bypass), scoped in-app to your admin pages, no gate required:
@@ -652,7 +652,7 @@ const role = await auth.role({ from: { table: "staff", idColumn: "user_id", role
652
652
if (role !== "operator") return Astro.redirect("/admin/login", 303);
653
653
```
654
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.
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, being per-function, would also gate the public 404s + `/admin/login`). If your console is instead a DEDICATED function/route, prefer the edge gate — it's cookie-aware, supports `onDeny: "redirect"` to sign-in, and caches the role lookup.
656
656
657
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