@pylonsync/functions 0.3.158 → 0.3.162
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 +1 -1
- package/src/define.ts +157 -28
- package/src/index.ts +2 -0
- package/src/runtime.ts +69 -8
- package/src/types.ts +76 -8
package/package.json
CHANGED
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
|
-
|
|
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
|
|
21
|
-
*
|
|
22
|
-
* `404 FN_NOT_FOUND` so
|
|
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
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
41
|
-
handler: (
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
*
|
|
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:
|
|
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", {
|
|
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:
|
|
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:
|
|
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
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
|
-
|
|
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, {
|
|
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, {
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|