Skip to content

Commit 4291d93

Browse files
MajorTalclaude
andauthored
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>
1 parent b90e9d4 commit 4291d93

2 files changed

Lines changed: 5 additions & 5 deletions

File tree

SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ Rules and footnotes:
411411
- **Deploy-time validation.** Missing table or column at activation fails with `DEPLOY_INVALID_ROLE_GATE` (422) *before* flipping the live release.
412412
- **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).
413413
- **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).
415415
- **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.
416416

417417
### Astro SSR runtime + ISR cache (v1.52+)

cli/llms-cli.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,11 +639,11 @@ Validation rules:
639639
- Empty `allowed`. Rejected with `INVALID_SPEC` — set the role values explicitly.
640640
- 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.
641641

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.
643643

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.
645645

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:
647647

648648
```ts
649649
const { user } = await auth.requireRole("operator", { from: { table: "staff", idColumn: "user_id", roleColumn: "role" } });
@@ -652,7 +652,7 @@ const role = await auth.role({ from: { table: "staff", idColumn: "user_id", role
652652
if (role !== "operator") return Astro.redirect("/admin/login", 303);
653653
```
654654

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.
656656

657657
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)`.
658658

0 commit comments

Comments
 (0)