@pylonsync/functions 0.3.157 → 0.3.161

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.3.157",
3
+ "version": "0.3.161",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/define.ts CHANGED
@@ -2,6 +2,23 @@
2
2
  * Function definition constructors.
3
3
  *
4
4
  * These are the primary API for defining server-side functions.
5
+ *
6
+ * Each constructor (query / mutation / action) has two overloads:
7
+ *
8
+ * 1. `auth` omitted, or `auth: "user" | "admin"` → the framework
9
+ * enforces a real signed-in user, so the handler's
10
+ * `ctx.auth.userId` is narrowed from `string | null` to
11
+ * `string`. The redundant `if (!ctx.auth.userId) throw …`
12
+ * check disappears from app code.
13
+ *
14
+ * 2. `auth: "public" | "guest"` → the function is reachable by
15
+ * anonymous callers, so `ctx.auth.userId` stays `string | null`
16
+ * and the handler must check it manually if relevant.
17
+ *
18
+ * The framework gate happens BEFORE the handler runs (in Rust,
19
+ * inside the router). A handler can't accidentally leak data by
20
+ * forgetting an auth check — when `auth: "user"` is in effect,
21
+ * an anonymous request never reaches the handler at all.
5
22
  */
6
23
 
7
24
  import type {
@@ -10,37 +27,80 @@ import type {
10
27
  MutationCtx,
11
28
  ActionCtx,
12
29
  Validator,
30
+ AuthMode,
13
31
  } from "./types";
14
32
 
15
- interface QueryDef<TArgs, TReturn> {
33
+ /** Default auth mode applied when a definition omits `auth`. */
34
+ const DEFAULT_AUTH: AuthMode = "user";
35
+
36
+ interface CommonDef {
16
37
  args?: Record<string, Validator>;
17
- handler: (ctx: QueryCtx, args: TArgs) => Promise<TReturn>;
18
38
  /**
19
- * When true, the function is callable only via `ctx.runQuery()`
20
- * from another function — never via the public `/api/fn/<name>`
21
- * HTTP endpoint. The router refuses external calls with
22
- * `404 FN_NOT_FOUND` so probing can't even confirm the name exists.
39
+ * When true, the function is callable only via `ctx.runQuery()` /
40
+ * `ctx.runMutation()` / `ctx.runAction()` from another function
41
+ * never via the public `/api/fn/<name>` HTTP endpoint. The
42
+ * router refuses external calls with `404 FN_NOT_FOUND` so
43
+ * probing can't even confirm the name exists.
23
44
  *
24
- * Use for queries that are meant as helpers for trusted action /
25
- * mutation flows but would be unsafe if any caller could invoke
26
- * them directly (e.g. they trust args without re-checking caller
27
- * authority).
45
+ * Use for helper functions that are safe inside trusted
46
+ * wrappers but unsafe if any caller could invoke them directly
47
+ * (e.g. they trust args without re-checking caller authority).
48
+ *
49
+ * Internal functions inherit the wrapping handler's auth — the
50
+ * `auth` field has no effect when `internal: true`. The router
51
+ * isn't reachable anyway, and the caller has already passed its
52
+ * own gate.
28
53
  */
29
54
  internal?: boolean;
30
55
  }
31
56
 
32
- interface MutationDef<TArgs, TReturn> {
33
- args?: Record<string, Validator>;
34
- handler: (ctx: MutationCtx, args: TArgs) => Promise<TReturn>;
35
- /** See QueryDef.internal — applies the same way to mutations. */
36
- internal?: boolean;
57
+ interface QueryDefRequired<TArgs, TReturn> extends CommonDef {
58
+ /** Defaults to `"user"`. See [`AuthMode`] for the full surface. */
59
+ auth?: "user" | "admin";
60
+ handler: (
61
+ ctx: QueryCtx<"required">,
62
+ args: TArgs,
63
+ ) => Promise<TReturn>;
37
64
  }
38
65
 
39
- interface ActionDef<TArgs, TReturn> {
40
- args?: Record<string, Validator>;
41
- handler: (ctx: ActionCtx, args: TArgs) => Promise<TReturn>;
42
- /** See QueryDef.internal — applies the same way to actions. */
43
- internal?: boolean;
66
+ interface QueryDefOptional<TArgs, TReturn> extends CommonDef {
67
+ auth: "public" | "guest";
68
+ handler: (
69
+ ctx: QueryCtx<"optional">,
70
+ args: TArgs,
71
+ ) => Promise<TReturn>;
72
+ }
73
+
74
+ interface MutationDefRequired<TArgs, TReturn> extends CommonDef {
75
+ auth?: "user" | "admin";
76
+ handler: (
77
+ ctx: MutationCtx<"required">,
78
+ args: TArgs,
79
+ ) => Promise<TReturn>;
80
+ }
81
+
82
+ interface MutationDefOptional<TArgs, TReturn> extends CommonDef {
83
+ auth: "public" | "guest";
84
+ handler: (
85
+ ctx: MutationCtx<"optional">,
86
+ args: TArgs,
87
+ ) => Promise<TReturn>;
88
+ }
89
+
90
+ interface ActionDefRequired<TArgs, TReturn> extends CommonDef {
91
+ auth?: "user" | "admin";
92
+ handler: (
93
+ ctx: ActionCtx<"required">,
94
+ args: TArgs,
95
+ ) => Promise<TReturn>;
96
+ }
97
+
98
+ interface ActionDefOptional<TArgs, TReturn> extends CommonDef {
99
+ auth: "public" | "guest";
100
+ handler: (
101
+ ctx: ActionCtx<"optional">,
102
+ args: TArgs,
103
+ ) => Promise<TReturn>;
44
104
  }
45
105
 
46
106
  /**
@@ -52,24 +112,46 @@ interface ActionDef<TArgs, TReturn> {
52
112
  * @example
53
113
  * ```typescript
54
114
  * export default query({
115
+ * // auth: "user" is the default — ctx.auth.userId is `string`,
116
+ * // not `string | null`, inside the handler.
55
117
  * args: { auctionId: v.string() },
56
118
  * async handler(ctx, args) {
57
119
  * return ctx.db.query("Lot", {
58
120
  * auctionId: args.auctionId,
59
- * $order: { closesAt: "asc" },
121
+ * authorId: ctx.auth.userId,
60
122
  * });
61
123
  * },
62
124
  * });
63
125
  * ```
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * // Explicitly public — landing-page count, never auth-shaped.
130
+ * export default query({
131
+ * auth: "public",
132
+ * async handler(ctx) {
133
+ * return ctx.db.query("PublicPost", { published: true });
134
+ * },
135
+ * });
136
+ * ```
64
137
  */
65
138
  export function query<TArgs = Record<string, unknown>, TReturn = unknown>(
66
- def: QueryDef<TArgs, TReturn>
139
+ def: QueryDefRequired<TArgs, TReturn>,
140
+ ): FnDefinition<TArgs, TReturn>;
141
+ export function query<TArgs = Record<string, unknown>, TReturn = unknown>(
142
+ def: QueryDefOptional<TArgs, TReturn>,
143
+ ): FnDefinition<TArgs, TReturn>;
144
+ export function query<TArgs, TReturn>(
145
+ def:
146
+ | QueryDefRequired<TArgs, TReturn>
147
+ | QueryDefOptional<TArgs, TReturn>,
67
148
  ): FnDefinition<TArgs, TReturn> {
68
149
  return {
69
150
  type: "query",
70
151
  args: def.args,
71
- handler: def.handler,
152
+ handler: def.handler as FnDefinition<TArgs, TReturn>["handler"],
72
153
  internal: def.internal,
154
+ auth: def.auth ?? DEFAULT_AUTH,
73
155
  };
74
156
  }
75
157
 
@@ -88,22 +170,37 @@ export function query<TArgs = Record<string, unknown>, TReturn = unknown>(
88
170
  * export default mutation({
89
171
  * args: { lotId: v.string(), amount: v.number() },
90
172
  * async handler(ctx, args) {
173
+ * // ctx.auth.userId is `string` (not nullable) — the runtime
174
+ * // already enforced `auth: "user"` (default) before reaching here.
91
175
  * const lot = await ctx.db.get("Lot", args.lotId);
92
176
  * if (!lot) throw ctx.error("NOT_FOUND", "Lot not found");
93
- * await ctx.db.insert("Bid", { lotId: args.lotId, amount: args.amount });
177
+ * await ctx.db.insert("Bid", {
178
+ * lotId: args.lotId,
179
+ * amount: args.amount,
180
+ * bidderId: ctx.auth.userId,
181
+ * });
94
182
  * return { accepted: true };
95
183
  * },
96
184
  * });
97
185
  * ```
98
186
  */
99
187
  export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
100
- def: MutationDef<TArgs, TReturn>
188
+ def: MutationDefRequired<TArgs, TReturn>,
189
+ ): FnDefinition<TArgs, TReturn>;
190
+ export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
191
+ def: MutationDefOptional<TArgs, TReturn>,
192
+ ): FnDefinition<TArgs, TReturn>;
193
+ export function mutation<TArgs, TReturn>(
194
+ def:
195
+ | MutationDefRequired<TArgs, TReturn>
196
+ | MutationDefOptional<TArgs, TReturn>,
101
197
  ): FnDefinition<TArgs, TReturn> {
102
198
  return {
103
199
  type: "mutation",
104
200
  args: def.args,
105
- handler: def.handler,
201
+ handler: def.handler as FnDefinition<TArgs, TReturn>["handler"],
106
202
  internal: def.internal,
203
+ auth: def.auth ?? DEFAULT_AUTH,
107
204
  };
108
205
  }
109
206
 
@@ -116,6 +213,13 @@ export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
116
213
  *
117
214
  * Actions are NOT automatically retried because they may have side effects.
118
215
  *
216
+ * **Important:** policies don't gate actions — `auth: "user"` (the
217
+ * default) is the only thing protecting an action from anonymous calls.
218
+ * An action that charges Stripe, hits a private API, or reads a
219
+ * secret will respond happily to anonymous POSTs unless this gate
220
+ * fires. Pick `auth: "public"` explicitly only when the endpoint is
221
+ * meant to be open (a webhook receiver, a public form submit, …).
222
+ *
119
223
  * @example
120
224
  * ```typescript
121
225
  * export default action({
@@ -127,14 +231,39 @@ export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
127
231
  * },
128
232
  * });
129
233
  * ```
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * // GitHub webhook receiver — public because GitHub doesn't sign
238
+ * // in, the handler verifies the signature itself, then optionally
239
+ * // elevates.
240
+ * export default action({
241
+ * auth: "public",
242
+ * async handler(ctx) {
243
+ * const ok = verifyGithubSignature(secret, ctx.request!.rawBody, sig);
244
+ * if (!ok) throw ctx.error("INVALID_SIGNATURE", "bad sig");
245
+ * await ctx.auth.elevate({ admin: true, reason: "github webhook hmac" });
246
+ * // …
247
+ * },
248
+ * });
249
+ * ```
130
250
  */
131
251
  export function action<TArgs = Record<string, unknown>, TReturn = unknown>(
132
- def: ActionDef<TArgs, TReturn>
252
+ def: ActionDefRequired<TArgs, TReturn>,
253
+ ): FnDefinition<TArgs, TReturn>;
254
+ export function action<TArgs = Record<string, unknown>, TReturn = unknown>(
255
+ def: ActionDefOptional<TArgs, TReturn>,
256
+ ): FnDefinition<TArgs, TReturn>;
257
+ export function action<TArgs, TReturn>(
258
+ def:
259
+ | ActionDefRequired<TArgs, TReturn>
260
+ | ActionDefOptional<TArgs, TReturn>,
133
261
  ): FnDefinition<TArgs, TReturn> {
134
262
  return {
135
263
  type: "action",
136
264
  args: def.args,
137
- handler: def.handler,
265
+ handler: def.handler as FnDefinition<TArgs, TReturn>["handler"],
138
266
  internal: def.internal,
267
+ auth: def.auth ?? DEFAULT_AUTH,
139
268
  };
140
269
  }
package/src/index.ts CHANGED
@@ -31,5 +31,7 @@ export type {
31
31
  Stream,
32
32
  Scheduler,
33
33
  AuthInfo,
34
+ AuthMode,
35
+ AuthRequirement,
34
36
  FnDefinition,
35
37
  } from "./types";
package/src/runtime.ts CHANGED
@@ -287,15 +287,32 @@ function rpc(callId: string, msg: Record<string, unknown>): Promise<unknown> {
287
287
  // Context builders
288
288
  // ---------------------------------------------------------------------------
289
289
 
290
- function buildDbReader(callId: string): DbReader {
290
+ function buildDbReader(callId: string, unsafeOp = false): DbReader {
291
291
  // All DB ops use rpcDb so Promise.all over ctx.db reads can run in
292
292
  // parallel without colliding on the outer call_id key.
293
- return {
293
+ //
294
+ // `unsafeOp` flag: when true, the emitted DbOp messages carry
295
+ // `unsafe_op: true` so the Rust side knows to skip the
296
+ // caller-aware policy gate (in Phase 2 — see
297
+ // pylon-functions/protocol.rs). Plain ctx.db.* leaves the flag
298
+ // off (the safe default); ctx.db.unsafe.* sets it.
299
+ const reader: DbReader = {
294
300
  async get(entity, id) {
295
- return (await rpcDb(callId, { type: "db", op: "get", entity, id })) as any;
301
+ return (await rpcDb(callId, {
302
+ type: "db",
303
+ op: "get",
304
+ entity,
305
+ id,
306
+ unsafe_op: unsafeOp,
307
+ })) as any;
296
308
  },
297
309
  async list(entity) {
298
- return (await rpcDb(callId, { type: "db", op: "list", entity })) as any;
310
+ return (await rpcDb(callId, {
311
+ type: "db",
312
+ op: "list",
313
+ entity,
314
+ unsafe_op: unsafeOp,
315
+ })) as any;
299
316
  },
300
317
  async lookup(entity, field, value) {
301
318
  return (await rpcDb(callId, {
@@ -304,6 +321,7 @@ function buildDbReader(callId: string): DbReader {
304
321
  entity,
305
322
  field,
306
323
  value,
324
+ unsafe_op: unsafeOp,
307
325
  })) as any;
308
326
  },
309
327
  async query(entity, filter) {
@@ -312,6 +330,7 @@ function buildDbReader(callId: string): DbReader {
312
330
  op: "query",
313
331
  entity,
314
332
  data: filter,
333
+ unsafe_op: unsafeOp,
315
334
  })) as any;
316
335
  },
317
336
  async queryGraph(query) {
@@ -320,6 +339,7 @@ function buildDbReader(callId: string): DbReader {
320
339
  op: "query_graph",
321
340
  entity: "",
322
341
  data: query,
342
+ unsafe_op: unsafeOp,
323
343
  })) as any;
324
344
  },
325
345
  async paginate(entity, opts) {
@@ -332,6 +352,7 @@ function buildDbReader(callId: string): DbReader {
332
352
  entity,
333
353
  after: opts.cursor ?? undefined,
334
354
  limit: numItems,
355
+ unsafe_op: unsafeOp,
335
356
  })) as any;
336
357
  },
337
358
  async search(entity, query) {
@@ -340,21 +361,34 @@ function buildDbReader(callId: string): DbReader {
340
361
  op: "search",
341
362
  entity,
342
363
  data: query,
364
+ unsafe_op: unsafeOp,
343
365
  })) as any;
344
366
  },
345
367
  };
368
+ if (!unsafeOp) {
369
+ (reader as DbReader & { unsafe: DbReader }).unsafe = buildDbReader(
370
+ callId,
371
+ true,
372
+ );
373
+ }
374
+ return reader;
346
375
  }
347
376
 
348
- function buildDbWriter(callId: string): DbWriter {
349
- const reader = buildDbReader(callId);
350
- return {
351
- ...reader,
377
+ function buildDbWriter(callId: string, unsafeOp = false): DbWriter {
378
+ // Drop the reader's `unsafe` shortcut before spreading — the
379
+ // writer needs its own (which we attach below). Without this
380
+ // strip, `writer.unsafe` would be a DbReader and the type
381
+ // narrows incorrectly.
382
+ const { unsafe: _ignored, ...readerOps } = buildDbReader(callId, unsafeOp);
383
+ const writer: DbWriter = {
384
+ ...readerOps,
352
385
  async insert(entity, data) {
353
386
  const r = (await rpcDb(callId, {
354
387
  type: "db",
355
388
  op: "insert",
356
389
  entity,
357
390
  data,
391
+ unsafe_op: unsafeOp,
358
392
  })) as { id: string };
359
393
  return r.id;
360
394
  },
@@ -365,6 +399,7 @@ function buildDbWriter(callId: string): DbWriter {
365
399
  entity,
366
400
  id,
367
401
  data,
402
+ unsafe_op: unsafeOp,
368
403
  })) as { updated: boolean };
369
404
  return r.updated;
370
405
  },
@@ -374,6 +409,7 @@ function buildDbWriter(callId: string): DbWriter {
374
409
  op: "delete",
375
410
  entity,
376
411
  id,
412
+ unsafe_op: unsafeOp,
377
413
  })) as { deleted: boolean };
378
414
  return r.deleted;
379
415
  },
@@ -385,6 +421,7 @@ function buildDbWriter(callId: string): DbWriter {
385
421
  id,
386
422
  relation,
387
423
  target_id: targetId,
424
+ unsafe_op: unsafeOp,
388
425
  })) as { linked: boolean };
389
426
  return r.linked;
390
427
  },
@@ -395,6 +432,7 @@ function buildDbWriter(callId: string): DbWriter {
395
432
  entity,
396
433
  id,
397
434
  relation,
435
+ unsafe_op: unsafeOp,
398
436
  })) as { unlinked: boolean };
399
437
  return r.unlinked;
400
438
  },
@@ -406,9 +444,27 @@ function buildDbWriter(callId: string): DbWriter {
406
444
  type: "db",
407
445
  op: "advisory_lock",
408
446
  entity: key,
447
+ unsafe_op: unsafeOp,
409
448
  });
410
449
  },
411
450
  };
451
+ // Top-level `ctx.db` is the safe path. `ctx.db.unsafe` is the
452
+ // escape hatch — same surface, every emitted op carries
453
+ // `unsafe_op: true` so the future caller-aware policy gate
454
+ // (PYLON_STRICT_FN_POLICIES) skips enforcement. Use sparingly,
455
+ // with a justifying comment, ideally in code that runs only
456
+ // from server-internal callers (webhooks, cron sweeps, admin
457
+ // tools).
458
+ //
459
+ // Self-reference would create an infinite loop on JSON
460
+ // serialization; only assign on the writer's `.unsafe` once.
461
+ if (!unsafeOp) {
462
+ (writer as DbWriter & { unsafe: DbWriter }).unsafe = buildDbWriter(
463
+ callId,
464
+ true,
465
+ );
466
+ }
467
+ return writer;
412
468
  }
413
469
 
414
470
  function buildStream(callId: string): Stream {
@@ -758,6 +814,11 @@ async function main() {
758
814
  // requests for internal fns; the Bun runtime here doesn't gate
759
815
  // (nested calls go through the same dispatcher).
760
816
  internal: def.internal === true,
817
+ // Declarative auth gate, enforced by the Rust router before the
818
+ // handler is invoked. Defaults to "user" when the TS def omits
819
+ // it — secure by default. See `packages/functions/src/define.ts`
820
+ // for the developer-facing AuthMode docs.
821
+ auth: def.auth ?? "user",
761
822
  }));
762
823
  send({ type: "ready", functions });
763
824
 
package/src/types.ts CHANGED
@@ -6,8 +6,43 @@
6
6
  // Auth
7
7
  // ---------------------------------------------------------------------------
8
8
 
9
- export interface AuthInfo {
10
- userId: string | null;
9
+ /**
10
+ * Declarative auth requirement for a function. The framework
11
+ * enforces this BEFORE the handler runs — if the caller doesn't
12
+ * meet the bar, the request rejects with a typed error and the
13
+ * handler is never invoked.
14
+ *
15
+ * Functions default to `"user"` (signed-in required) when this
16
+ * field is omitted. That's the secure-by-default position: a
17
+ * forgotten `if (!ctx.auth.userId)` check never leaks data,
18
+ * because the runtime made the check before the handler ran.
19
+ *
20
+ * Modes:
21
+ * - `"public"` — anyone, including unauthenticated callers. Use
22
+ * for healthchecks, landing-page form submits, intentionally-open
23
+ * webhooks. Must be explicit; never the default.
24
+ * - `"guest"` — anonymous-with-stable-id sessions count, plus
25
+ * any authenticated user. Use for cart-style pre-login state.
26
+ * - `"user"` — a real signed-in user (default). Guest sessions
27
+ * are rejected. Inside the handler, `ctx.auth.userId` is
28
+ * narrowed from `string | null` to `string` so the redundant
29
+ * null check can be dropped.
30
+ * - `"admin"` — `ctx.auth.isAdmin === true`. Use for ops
31
+ * endpoints exposed via `/api/fn/...`.
32
+ */
33
+ export type AuthMode = "public" | "guest" | "user" | "admin";
34
+
35
+ /**
36
+ * `userId` shape narrows based on the function's declared auth
37
+ * requirement. `auth: "user"` and `auth: "admin"` both guarantee
38
+ * a real signed-in user, so the handler sees a non-null string.
39
+ * `auth: "public"` and `auth: "guest"` allow anonymous callers,
40
+ * so the handler must keep checking.
41
+ */
42
+ export type AuthRequirement = "required" | "optional";
43
+
44
+ export interface AuthInfo<R extends AuthRequirement = "optional"> {
45
+ userId: R extends "required" ? string : string | null;
11
46
  isAdmin: boolean;
12
47
  /** Active tenant id (selected organization) for multi-tenant apps.
13
48
  * Null when the session hasn't selected one. */
@@ -49,6 +84,26 @@ export interface AuthInfo {
49
84
  // ---------------------------------------------------------------------------
50
85
 
51
86
  export interface DbReader {
87
+ /**
88
+ * Escape hatch: same surface as `ctx.db` but operations bypass
89
+ * the framework's caller-aware policy gate (gated by
90
+ * `PYLON_STRICT_FN_POLICIES=1`, Phase 2). Use sparingly, only
91
+ * in code that runs from a trusted server-internal context —
92
+ * webhook receivers (after signature verification), scheduled
93
+ * cron sweeps, admin tooling. The plain `ctx.db.*` reads
94
+ * already work for the caller's-own-data case; `ctx.db.unsafe`
95
+ * is the answer when you genuinely need cross-tenant or
96
+ * cross-user reads.
97
+ *
98
+ * Every call should carry a justifying comment per codebase
99
+ * convention. A future `pylon lint` rule will flag bare
100
+ * `ctx.db.unsafe.*` without a comment immediately above.
101
+ *
102
+ * Optional on the type because old Pylon runtimes don't ship
103
+ * it; new code that targets v0.3.161+ can rely on the field.
104
+ */
105
+ unsafe?: DbReader;
106
+
52
107
  /** Get a single row by ID. Returns null if not found. */
53
108
  get(entity: string, id: string): Promise<Record<string, unknown> | null>;
54
109
 
@@ -141,6 +196,13 @@ export interface SearchResult<T = Record<string, unknown>> {
141
196
  // ---------------------------------------------------------------------------
142
197
 
143
198
  export interface DbWriter extends DbReader {
199
+ /**
200
+ * Escape hatch — same shape as [`DbReader.unsafe`] but with the
201
+ * write surface (insert/update/delete/link/unlink/advisoryLock).
202
+ * Overrides the inherited read-only `unsafe` from DbReader.
203
+ */
204
+ unsafe?: DbWriter;
205
+
144
206
  /** Insert a new row. Returns the generated ID. */
145
207
  insert(entity: string, data: Record<string, unknown>): Promise<string>;
146
208
 
@@ -248,17 +310,17 @@ export interface EmailSender {
248
310
  // ---------------------------------------------------------------------------
249
311
 
250
312
  /** Context for query handlers (read-only). */
251
- export interface QueryCtx {
313
+ export interface QueryCtx<R extends AuthRequirement = "optional"> {
252
314
  db: DbReader;
253
- auth: AuthInfo;
315
+ auth: AuthInfo<R>;
254
316
  /** Environment variables / secrets. */
255
317
  env: Record<string, string>;
256
318
  }
257
319
 
258
320
  /** Context for mutation handlers (read + write, transactional). */
259
- export interface MutationCtx {
321
+ export interface MutationCtx<R extends AuthRequirement = "optional"> {
260
322
  db: DbWriter;
261
- auth: AuthInfo;
323
+ auth: AuthInfo<R>;
262
324
  stream: Stream;
263
325
  scheduler: Scheduler;
264
326
  /** Environment variables / secrets. */
@@ -268,8 +330,8 @@ export interface MutationCtx {
268
330
  }
269
331
 
270
332
  /** Context for action handlers (external I/O, non-transactional). */
271
- export interface ActionCtx {
272
- auth: AuthInfo;
333
+ export interface ActionCtx<R extends AuthRequirement = "optional"> {
334
+ auth: AuthInfo<R>;
273
335
  stream: Stream;
274
336
  scheduler: Scheduler;
275
337
  /** Send transactional email via the runtime's configured provider. */
@@ -336,6 +398,12 @@ export interface FnDefinition<TArgs = unknown, TReturn = unknown> {
336
398
  * for execution.
337
399
  */
338
400
  internal?: boolean;
401
+ /**
402
+ * Auth requirement enforced by the runtime before the handler is
403
+ * invoked. Defaults to `"user"` — every function is signed-in only
404
+ * unless explicitly opted out via `auth: "public"`. See [`AuthMode`].
405
+ */
406
+ auth?: AuthMode;
339
407
  }
340
408
 
341
409
  // ---------------------------------------------------------------------------