@run402/functions 4.2.0 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +28 -31
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  In-function helper library for [Run402](https://run402.com) serverless functions. Imported _inside_ a deployed function — gives you typed access to the caller's database (RLS-respecting) and the project's admin database, the caller's auth, the project's mailbox, AI helpers, runtime asset uploads, and the project's cursored event feed.
4
4
 
5
- Run402's first-class people/agent **control-plane principals** are distinct from the deployed app's tenant callers described in this package. Here, `getUser(req)`, JWT roles, and RLS identify an end user of the application; they do not expose or replace the Run402 organization membership, grant, delegate, or Buzz identity model.
5
+ Run402's first-class people/agent **control-plane principals** are distinct from the deployed app's tenant callers described in this package. Here, `auth.user()`, verified tenant identity, and RLS identify an end user of the application; they do not expose or replace the Run402 organization membership, grant, delegate, or Buzz identity model.
6
6
 
7
7
  ```ts
8
- import { db, adminDb, getUser, email, ai, assets } from "@run402/functions";
8
+ import { db, adminDb, auth, email, ai, assets } from "@run402/functions";
9
9
 
10
10
  export default async (req: Request) => {
11
- const user = await getUser(req);
11
+ const user = await auth.user();
12
12
  if (!user) return new Response("unauthorized", { status: 401 });
13
13
 
14
14
  const mine = await db(req).from("items").select("*").eq("user_id", user.id);
@@ -107,25 +107,30 @@ Returns `AdminSqlResult` (exported type): `{ status, schema, rows, row_count, fi
107
107
 
108
108
  The result is the **envelope, never a bare row array** — iterating the result itself or reading `.length` on it silently yields nothing; destructure `rows`. A 2xx whose body is not `{ rows: [...] }` throws `R402DbError` with `code: "R402_DB_SQL_RESULT_SHAPE"` rather than resolving. An omitted or **empty** `params` array sends the query as `text/plain` with no parameter binding, so `$1` placeholders fail server-side — pass a non-empty array whenever the query uses placeholders.
109
109
 
110
- ## `getUser(req)` — caller identity
110
+ ## `auth.user()` — caller identity
111
111
 
112
- Verifies the caller's JWT and returns the user, or `null` for unauthenticated requests.
112
+ Reads the verified request-scoped actor, or returns `null` when unauthenticated. The deployed runtime supplies the context; do not pass a Request.
113
113
 
114
+ <!-- auth-identity-example -->
114
115
  ```ts
115
- const user = await getUser(req);
116
+ import { auth } from "@run402/functions";
117
+
118
+ const user = await auth.user();
116
119
  if (!user) return new Response("unauthorized", { status: 401 });
117
- // user: { id: string, email: string, role: "authenticated" | "project_admin" | ... }
120
+ return Response.json({ id: user.id, email: user.email });
118
121
  ```
119
122
 
120
- The function's own `RUN402_PROJECT_ID` is used to scope the verification.
123
+ For required identity, use `await auth.requireUser()` and let the platform handle its refusal. Actor identity is distinct from your application's membership or role: use `auth.requireRole(...)` / `auth.requireMembership(...)` for the supported cookie-session authorization flow, or a declarative function gate for direct API calls.
124
+
125
+ On tenant SSR routes, identity comes from the verified cookie-session envelope. Direct function invocations also support the project's verified user Bearer token. A browser SPA may use the public token API with an anon key and user access token; that is distinct from same-origin cookie-based SSR. Never put a service key in browser code.
121
126
 
122
- **Note on `user.role`:** this is the JWT system role (`anon`, `authenticated`, `project_admin`, …) — the same value PostgREST uses to evaluate RLS. It is **not** your application role from a declarative `requireRole` gate (admin/moderator/etc.). For the gate-resolved application role, use `getRole(req)` (see "Function-level auth gates" below). Don't write `getUser(req).role === "admin"` thinking you're checking the gate role — `"admin"` is not a JWT role.
127
+ The legacy bare helpers `getUser`, `getUserId`, and `getRole` are throwing sentinels, not compatibility implementations. Calling them raises `R402_AUTH_UNKNOWN_EXPORT`; deploy preflight rejects them too.
123
128
 
124
129
  ## Function-level auth gates
125
130
 
126
131
  A function can declare auth requirements directly on its `FunctionSpec`. When you set `requireAuth: true` or `requireRole: { ... }`, the gateway enforces them **before** invoking your function — unauthorized callers get `401` / `403` without your code running, and the gateway injects the resolved identity into request headers your function can trust.
127
132
 
128
- This lets you delete the hand-rolled "fetch JWT, query members table, check role, return 403" boilerplate from every privileged function. Declare the gate in your `FunctionSpec`; read the resolved identity with `getUserId(req)` and `getRole(req)`.
133
+ This lets you delete the hand-rolled "fetch JWT, query members table, check role, return 403" boilerplate from every privileged function. Declare the gate in your `FunctionSpec`; read the verified actor with `auth.user()` and, when a declarative gate ran, its application role from the injected request headers.
129
134
 
130
135
  ### Declaring a gate (deploy spec)
131
136
 
@@ -133,7 +138,8 @@ This lets you delete the hand-rolled "fetch JWT, query members table, check role
133
138
  import { run402 } from "@run402/sdk/node";
134
139
 
135
140
  const r = run402();
136
- await r.project(projectId).apply({
141
+ const project = await r.project(projectId);
142
+ await project.apply({
137
143
  functions: {
138
144
  patch: {
139
145
  set: {
@@ -178,15 +184,15 @@ await r.project(projectId).apply({
178
184
  ### Reading the gate result inside your function
179
185
 
180
186
  ```ts
181
- import { getUserId, getRole } from "@run402/functions";
187
+ import { auth } from "@run402/functions";
182
188
 
183
189
  export default async (req: Request): Promise<Response> => {
184
- const userId = getUserId(req); // string | null
185
- const role = getRole(req); // string | null
190
+ const userId = (await auth.user())?.id ?? null;
191
+ const role = req.headers.get("x-run402-user-role"); // application role, not JWT role
186
192
 
187
193
  // For a gated function reached through the gateway, both are guaranteed:
188
- // - getUserId(req) is non-null when requireAuth OR requireRole is on.
189
- // - getRole(req) is non-null when requireRole is on (and is one of `allowed`).
194
+ // - x-run402-user-id is set when requireAuth OR requireRole is on.
195
+ // - x-run402-user-role is set when requireRole is on (one of allowed).
190
196
  // The null case covers local invokes / direct Lambda tests / ungated functions.
191
197
 
192
198
  if (role === "admin") {
@@ -213,22 +219,13 @@ If a `requireRole` block references a table or column that doesn't exist in the
213
219
 
214
220
  Role lookups are cached per `(projectId, userId)` for `cacheTtl` seconds (default 60, max 600). **A demoted user keeps the cached role until the TTL expires** — for high-stakes operations where instant revocation matters, set `cacheTtl: 0` to issue a fresh lookup on every request. The cache is bypassed when no `requireRole` gate runs.
215
221
 
216
- ### Relationship to `getUser`
217
-
218
- `getUser(req)` decodes the JWT and gives you `{ id, role, email }` where `role` is the JWT system role. The gate-injected headers give you the gate-resolved identity:
219
-
220
- | Helper | Source | Role meaning |
221
- |---|---|---|
222
- | `getUser(req).id` | JWT `sub` (decoded in-function) | — |
223
- | `getUser(req).role` | JWT `role` claim | System role (`anon`, `authenticated`, `project_admin`) |
224
- | `getUserId(req)` | `x-run402-user-id` header (injected by gateway) | — |
225
- | `getRole(req)` | `x-run402-user-role` header (injected by gateway) | Application role from your `members` table |
222
+ ### Actor identity and application roles
226
223
 
227
- For a gated function reached through the gateway, `getUserId(req)` and `getUser(req).id` will agree. The gate-side helpers skip the JWT decode (the gateway already did it), so they're slightly cheaper and stringly-typed against the trusted headers; use them when the gate guarantees the identity.
224
+ `auth.user()` returns the verified actor, not an application role. A declarative gate supplies `x-run402-user-id` and (for `requireRole`) `x-run402-user-role`. Read these only inside the platform-invoked function, where the gateway strips spoofed headers and injects its verified result. Do not infer application authority from a JWT system role.
228
225
 
229
226
  ## `email.send(...)` — send mail from the project's mailbox
230
227
 
231
- Auto-discovers the project's mailbox on first call (the project must already have one — create it once with `run402 email create <slug>` or the `create_mailbox` MCP tool). After that the mailbox id is cached for the function's lifetime.
228
+ Auto-discovers the project's mailbox on first call (the project must already have one — create it once with `run402 email create <slug> --project <project-id>`). After that the mailbox id is cached for the function's lifetime.
232
229
 
233
230
  On Run402 Core, this uses the same `/mailboxes/v1` contract as Cloud. Deploy still happens through `run402 deploy apply --manifest`; outbound email is enabled separately by configuring the Core gateway's provider (for example SES) and creating a project mailbox/default. If Core has mailboxes but no outbound provider configured, `email.send()` throws `EmailConfigurationError` with code `PROVIDER_NOT_CONFIGURED` and setup `next_actions`.
234
231
 
@@ -306,14 +303,14 @@ return Response.json({ url: asset.immutableUrl ?? asset.url });
306
303
  Use a routed function when the browser should request an image at app runtime. Keep app-level auth/rate limits in your handler before calling `ai.generateImage`, especially for public routes.
307
304
 
308
305
  ```ts
309
- import { ai, getUser } from "@run402/functions";
306
+ import { ai, auth } from "@run402/functions";
310
307
 
311
308
  export default async function handler(req: Request): Promise<Response> {
312
309
  if (req.method !== "POST") {
313
310
  return new Response("method not allowed", { status: 405 });
314
311
  }
315
312
 
316
- const user = await getUser(req);
313
+ const user = await auth.user();
317
314
  if (!user) return new Response("unauthorized", { status: 401 });
318
315
 
319
316
  const { prompt } = await req.json() as { prompt?: string };
@@ -347,7 +344,7 @@ await events.emit("signature_completed", { request_id, signer }, {
347
344
  });
348
345
  ```
349
346
 
350
- Read it back with `GET /projects/v1/:project_id/events?source=app` (or `run402 events --source app`) — app events share the exact cursor/pagination/retention machinery as platform events (deploys, suspensions, transfers), just filtered to `source=app`.
347
+ Read it back with `run402 events --source app --project <project-id>` — app events share the exact cursor/pagination/retention machinery as platform events (deploys, suspensions, transfers), just filtered to `source=app`.
351
348
 
352
349
  **Vocabulary.** `type` must be flat snake_case matching `/^[a-z][a-z0-9_]{2,63}$/` — no dots, no `app_` prefix. Platform-registered type names (`deploy_activated`, `mailbox_suspended`, ...) are **reserved**: an app cannot impersonate a platform fact. This is enforced **server-side only** — `events.emit` does not pre-validate the grammar or check the reservation list locally; it sends `type` exactly as given. A bad grammar or a reserved name comes back as a thrown `Run402EventsPlatformError` with `code: "INVALID_EVENT_TYPE"` or `code: "RESERVED_EVENT_TYPE"` (both HTTP 400) — never a silently rewritten or dropped call.
353
350
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@run402/functions",
3
- "version": "4.2.0",
4
- "description": "In-function helper library for Run402 serverless functions - db, adminDb, getUser, email, ai, assets, verifyWebhook. Auto-bundled into deployed functions; also installable for local TypeScript autocomplete.",
3
+ "version": "4.2.1",
4
+ "description": "In-function helper library for Run402 serverless functions - db, adminDb, auth, email, ai, assets, verifyWebhook. Auto-bundled into deployed functions; also installable for local TypeScript autocomplete.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",