Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ Rules and footnotes:
- **Deploy-time validation.** Missing table or column at activation fails with `DEPLOY_INVALID_ROLE_GATE` (422) *before* flipping the live release.
- **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).
- **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.
- **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.
- **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).
- **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.

### Astro SSR runtime + ISR cache (v1.52+)
Expand Down
15 changes: 14 additions & 1 deletion cli/llms-cli.txt
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,20 @@ Validation rules:
- Empty `allowed`. Rejected with `INVALID_SPEC` — set the role values explicitly.
- 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.

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

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.

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:

```ts
const { user } = await auth.requireRole("operator", { from: { table: "staff", idColumn: "user_id", roleColumn: "role" } });
// or, for an .astro page (a throw in frontmatter renders a 500, not a redirect) use the non-throwing read:
const role = await auth.role({ from: { table: "staff", idColumn: "user_id", roleColumn: "role" } });
if (role !== "operator") return Astro.redirect("/admin/login", 303);
```

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.

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

Expand Down
Loading