@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.
- package/README.md +28 -31
- 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, `
|
|
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,
|
|
8
|
+
import { db, adminDb, auth, email, ai, assets } from "@run402/functions";
|
|
9
9
|
|
|
10
10
|
export default async (req: Request) => {
|
|
11
|
-
const user = await
|
|
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
|
-
## `
|
|
110
|
+
## `auth.user()` — caller identity
|
|
111
111
|
|
|
112
|
-
|
|
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
|
-
|
|
116
|
+
import { auth } from "@run402/functions";
|
|
117
|
+
|
|
118
|
+
const user = await auth.user();
|
|
116
119
|
if (!user) return new Response("unauthorized", { status: 401 });
|
|
117
|
-
|
|
120
|
+
return Response.json({ id: user.id, email: user.email });
|
|
118
121
|
```
|
|
119
122
|
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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 {
|
|
187
|
+
import { auth } from "@run402/functions";
|
|
182
188
|
|
|
183
189
|
export default async (req: Request): Promise<Response> => {
|
|
184
|
-
const userId =
|
|
185
|
-
const role =
|
|
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
|
-
// -
|
|
189
|
-
// -
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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 `
|
|
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.
|
|
4
|
-
"description": "In-function helper library for Run402 serverless functions - db, adminDb,
|
|
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",
|