@pylonsync/functions 0.3.292 → 0.3.294
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/dist/define.d.ts +195 -0
- package/dist/index.d.ts +25 -0
- package/dist/member.d.ts +8 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/slugify.d.ts +49 -0
- package/dist/ssr-client-boundary.d.ts +136 -0
- package/dist/ssr-client-bundler.d.ts +79 -0
- package/dist/ssr-fonts.d.ts +94 -0
- package/dist/ssr-form-runtime.d.ts +33 -0
- package/dist/ssr-runtime.d.ts +419 -0
- package/dist/testing.d.ts +31 -0
- package/dist/types.d.ts +561 -0
- package/dist/validators.d.ts +74 -0
- package/package.json +15 -7
- package/src/ssr-client-bundler.test.ts +32 -0
- package/src/ssr-client-bundler.ts +62 -2
- package/src/ssr-fonts.test.ts +303 -0
- package/src/ssr-fonts.ts +633 -0
- package/src/ssr-runtime.ts +34 -2
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the function system.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Declarative auth requirement for a function. The framework
|
|
6
|
+
* enforces this BEFORE the handler runs — if the caller doesn't
|
|
7
|
+
* meet the bar, the request rejects with a typed error and the
|
|
8
|
+
* handler is never invoked.
|
|
9
|
+
*
|
|
10
|
+
* Functions default to `"user"` (signed-in required) when this
|
|
11
|
+
* field is omitted. That's the secure-by-default position: a
|
|
12
|
+
* forgotten `if (!ctx.auth.userId)` check never leaks data,
|
|
13
|
+
* because the runtime made the check before the handler ran.
|
|
14
|
+
*
|
|
15
|
+
* Modes:
|
|
16
|
+
* - `"public"` — anyone, including unauthenticated callers. Use
|
|
17
|
+
* for healthchecks, landing-page form submits, intentionally-open
|
|
18
|
+
* webhooks. Must be explicit; never the default.
|
|
19
|
+
* - `"guest"` — anonymous-with-stable-id sessions count, plus
|
|
20
|
+
* any authenticated user. Use for cart-style pre-login state.
|
|
21
|
+
* - `"user"` — a real signed-in user (default). Guest sessions
|
|
22
|
+
* are rejected. Inside the handler, `ctx.auth.userId` is
|
|
23
|
+
* narrowed from `string | null` to `string` so the redundant
|
|
24
|
+
* null check can be dropped.
|
|
25
|
+
* - `"admin"` — `ctx.auth.isAdmin === true`. Use for ops
|
|
26
|
+
* endpoints exposed via `/api/fn/...`.
|
|
27
|
+
*/
|
|
28
|
+
export type AuthMode = "public" | "guest" | "user" | "admin";
|
|
29
|
+
/**
|
|
30
|
+
* `userId` shape narrows based on the function's declared auth
|
|
31
|
+
* requirement. `auth: "user"` and `auth: "admin"` both guarantee
|
|
32
|
+
* a real signed-in user, so the handler sees a non-null string.
|
|
33
|
+
* `auth: "public"` and `auth: "guest"` allow anonymous callers,
|
|
34
|
+
* so the handler must keep checking.
|
|
35
|
+
*/
|
|
36
|
+
export type AuthRequirement = "required" | "optional";
|
|
37
|
+
export interface AuthInfo<R extends AuthRequirement = "optional"> {
|
|
38
|
+
userId: R extends "required" ? string : string | null;
|
|
39
|
+
isAdmin: boolean;
|
|
40
|
+
/** Active tenant id (selected organization) for multi-tenant apps.
|
|
41
|
+
* Null when the session hasn't selected one. */
|
|
42
|
+
tenantId: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Promote the call's auth context after the handler has done its
|
|
45
|
+
* own authentication check (HMAC signature verification on a
|
|
46
|
+
* webhook, JWT validation, custom token check). Used by webhook
|
|
47
|
+
* receivers — they're necessarily public (external systems POST
|
|
48
|
+
* to them) but want to schedule internal:true workers after
|
|
49
|
+
* they've proven the request came from a trusted source.
|
|
50
|
+
*
|
|
51
|
+
* The framework does NOT verify the developer actually checked
|
|
52
|
+
* anything before calling this — that's on you. The `reason` is
|
|
53
|
+
* mandatory and gets logged at INFO with the function name so
|
|
54
|
+
* every elevation is auditable.
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Github webhook example:
|
|
58
|
+
* const ok = await verifyGithubSignature(secret, rawBody, sig);
|
|
59
|
+
* if (!ok) throw ctx.error("INVALID_SIGNATURE", "bad sig");
|
|
60
|
+
* await ctx.auth.elevate({
|
|
61
|
+
* admin: true,
|
|
62
|
+
* reason: "github webhook hmac verified",
|
|
63
|
+
* });
|
|
64
|
+
* // Now this works — caller_is_admin=true for the gate:
|
|
65
|
+
* await ctx.scheduler.runAfter(0, "deployProject", { deploymentId });
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* After calling `elevate({ admin: true })`, `auth.isAdmin` is also
|
|
69
|
+
* mutated to true locally so subsequent reads in the same handler
|
|
70
|
+
* see the new value.
|
|
71
|
+
*/
|
|
72
|
+
elevate(options: {
|
|
73
|
+
admin: boolean;
|
|
74
|
+
reason: string;
|
|
75
|
+
}): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
export interface DbReader {
|
|
78
|
+
/**
|
|
79
|
+
* Escape hatch: same surface as `ctx.db` but operations bypass
|
|
80
|
+
* the framework's caller-aware policy gate (gated by
|
|
81
|
+
* `PYLON_STRICT_FN_POLICIES=1`, Phase 2). Use sparingly, only
|
|
82
|
+
* in code that runs from a trusted server-internal context —
|
|
83
|
+
* webhook receivers (after signature verification), scheduled
|
|
84
|
+
* cron sweeps, admin tooling. The plain `ctx.db.*` reads
|
|
85
|
+
* already work for the caller's-own-data case; `ctx.db.unsafe`
|
|
86
|
+
* is the answer when you genuinely need cross-tenant or
|
|
87
|
+
* cross-user reads.
|
|
88
|
+
*
|
|
89
|
+
* Every call should carry a justifying comment per codebase
|
|
90
|
+
* convention. A future `pylon lint` rule will flag bare
|
|
91
|
+
* `ctx.db.unsafe.*` without a comment immediately above.
|
|
92
|
+
*
|
|
93
|
+
* Required on the type (every runtime since v0.3.161 ships it) —
|
|
94
|
+
* but absent on the unsafe surface itself, so `ctx.db.unsafe.unsafe`
|
|
95
|
+
* is a compile error rather than a runtime undefined.
|
|
96
|
+
*/
|
|
97
|
+
unsafe: Omit<DbReader, "unsafe">;
|
|
98
|
+
/** Get a single row by ID. Returns null if not found. */
|
|
99
|
+
get(entity: string, id: string): Promise<Record<string, unknown> | null>;
|
|
100
|
+
/** List all rows for an entity. */
|
|
101
|
+
list(entity: string): Promise<Record<string, unknown>[]>;
|
|
102
|
+
/** Lookup a row by a field value (e.g., email). */
|
|
103
|
+
lookup(entity: string, field: string, value: string): Promise<Record<string, unknown> | null>;
|
|
104
|
+
/** Query with filters ($gt, $lt, $in, $like, $order, $limit, etc.). */
|
|
105
|
+
query(entity: string, filter: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
106
|
+
/** Execute a graph query with nested relation includes. */
|
|
107
|
+
queryGraph(query: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
108
|
+
/**
|
|
109
|
+
* Faceted full-text search against an entity that declares a
|
|
110
|
+
* `search:` config. Mirrors the typed-client `client.search()` /
|
|
111
|
+
* the HTTP `/api/search/<entity>` shape.
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* const result = await ctx.db.search("Product", {
|
|
115
|
+
* query: "rust async",
|
|
116
|
+
* filters: { brand: "Atlas" },
|
|
117
|
+
* facets: ["category"],
|
|
118
|
+
* page: 0,
|
|
119
|
+
* pageSize: 20,
|
|
120
|
+
* });
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* Returns `{ hits, facetCounts, total, tookMs }`. Throws on
|
|
124
|
+
* entities without a `search:` config (`SEARCH_NOT_CONFIGURED`).
|
|
125
|
+
*/
|
|
126
|
+
search(entity: string, query: Record<string, unknown>): Promise<SearchResult>;
|
|
127
|
+
/**
|
|
128
|
+
* Cursor-paginated list. Pass `cursor` from a previous page's `nextCursor`
|
|
129
|
+
* to continue; pass `null` for the first page.
|
|
130
|
+
*
|
|
131
|
+
* ```ts
|
|
132
|
+
* const { page, nextCursor, isDone } =
|
|
133
|
+
* await ctx.db.paginate("Order", { cursor: null, numItems: 50 });
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* `numItems` is clamped to [1, 1000]; the server honors the clamp.
|
|
137
|
+
*/
|
|
138
|
+
paginate(entity: string, opts: {
|
|
139
|
+
cursor: string | null;
|
|
140
|
+
numItems: number;
|
|
141
|
+
}): Promise<PaginationResult>;
|
|
142
|
+
}
|
|
143
|
+
/** Result shape for [`DbReader.paginate`]. */
|
|
144
|
+
export interface PaginationResult<T = Record<string, unknown>> {
|
|
145
|
+
/** Rows in this page. */
|
|
146
|
+
page: T[];
|
|
147
|
+
/** Cursor to pass to the next `paginate` call. `null` when exhausted. */
|
|
148
|
+
nextCursor: string | null;
|
|
149
|
+
/** True when there are no more rows after this page. */
|
|
150
|
+
isDone: boolean;
|
|
151
|
+
}
|
|
152
|
+
/** Result shape for [`DbReader.search`]. */
|
|
153
|
+
export interface SearchResult<T = Record<string, unknown>> {
|
|
154
|
+
/** Ranked (or sorted) hit rows. */
|
|
155
|
+
hits: T[];
|
|
156
|
+
/** `{facet_name: {value: count}}` — counts excluded for the
|
|
157
|
+
* active filter on the same facet (standard exclusion pattern). */
|
|
158
|
+
facetCounts: Record<string, Record<string, number>>;
|
|
159
|
+
/** Total hit count before pagination. */
|
|
160
|
+
total: number;
|
|
161
|
+
/** Milliseconds spent in the search engine. */
|
|
162
|
+
tookMs: number;
|
|
163
|
+
}
|
|
164
|
+
export interface DbWriter extends DbReader {
|
|
165
|
+
/**
|
|
166
|
+
* Escape hatch — same shape as [`DbReader.unsafe`] but with the
|
|
167
|
+
* write surface (insert/update/delete/link/unlink/advisoryLock).
|
|
168
|
+
* Overrides the inherited read-only `unsafe` from DbReader.
|
|
169
|
+
*/
|
|
170
|
+
unsafe: Omit<DbWriter, "unsafe">;
|
|
171
|
+
/** Insert a new row. Returns the generated ID. */
|
|
172
|
+
insert(entity: string, data: Record<string, unknown>): Promise<string>;
|
|
173
|
+
/** Update a row by ID. Returns true if the row existed. */
|
|
174
|
+
update(entity: string, id: string, data: Record<string, unknown>): Promise<boolean>;
|
|
175
|
+
/** Delete a row by ID. Returns true if the row existed. */
|
|
176
|
+
delete(entity: string, id: string): Promise<boolean>;
|
|
177
|
+
/** Link two entities via a relation. */
|
|
178
|
+
link(entity: string, id: string, relation: string, targetId: string): Promise<boolean>;
|
|
179
|
+
/** Unlink a relation (set FK to null). */
|
|
180
|
+
unlink(entity: string, id: string, relation: string): Promise<boolean>;
|
|
181
|
+
/**
|
|
182
|
+
* Acquire a transaction-scoped advisory lock on `key`. Held until
|
|
183
|
+
* the mutation tx commits or rolls back. Two concurrent mutations
|
|
184
|
+
* holding the same key serialize on Postgres; on SQLite this is a
|
|
185
|
+
* noop because writers are already serialized at the connection
|
|
186
|
+
* level.
|
|
187
|
+
*
|
|
188
|
+
* Use this to close TOCTOU windows on quota / uniqueness checks:
|
|
189
|
+
* call `advisoryLock` BEFORE the count query so the second tx
|
|
190
|
+
* blocks on the first's commit before observing state.
|
|
191
|
+
*
|
|
192
|
+
* Example:
|
|
193
|
+
* ```ts
|
|
194
|
+
* await ctx.db.advisoryLock(`org_count:${ctx.auth.userId}`);
|
|
195
|
+
* const orgs = await ctx.db.query("Organization", { createdBy: userId });
|
|
196
|
+
* if (orgs.length >= cap) throw ctx.error("QUOTA_EXCEEDED", "...");
|
|
197
|
+
* await ctx.db.insert("Organization", { ... });
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
advisoryLock(key: string): Promise<void>;
|
|
201
|
+
}
|
|
202
|
+
export interface Stream {
|
|
203
|
+
/** Write a text chunk to the client (SSE). */
|
|
204
|
+
write(data: string): void;
|
|
205
|
+
/** Write a typed SSE event. */
|
|
206
|
+
writeEvent(event: string, data: string): void;
|
|
207
|
+
}
|
|
208
|
+
export interface Scheduler {
|
|
209
|
+
/** Schedule a function to run after a delay (milliseconds). */
|
|
210
|
+
runAfter(delayMs: number, fnName: string, args: Record<string, unknown>): Promise<string>;
|
|
211
|
+
/** Schedule a function to run at a specific time (Unix ms). */
|
|
212
|
+
runAt(timestamp: number, fnName: string, args: Record<string, unknown>): Promise<string>;
|
|
213
|
+
/** Cancel a previously scheduled function. */
|
|
214
|
+
cancel(scheduleId: string): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Transactional email transport.
|
|
218
|
+
*
|
|
219
|
+
* Sends through whatever provider the runtime is configured for
|
|
220
|
+
* (PYLON_EMAIL_PROVIDER env var → SendGrid / Resend / Stack0 / SMTP /
|
|
221
|
+
* webhook). Available on action ctx only — sending email is external
|
|
222
|
+
* I/O, not allowed in mutation transactions.
|
|
223
|
+
*
|
|
224
|
+
* This is the APP email channel (`PYLON_EMAIL_*`): arbitrary recipient
|
|
225
|
+
* and body, so it must be the app's own provider. It is deliberately
|
|
226
|
+
* separate from Pylon's built-in auth emails (codes / password reset /
|
|
227
|
+
* invitations), which send via a `PYLON_AUTH_EMAIL_*` channel. On Pylon
|
|
228
|
+
* Cloud the auth channel may be a shared, locked-down platform key, so
|
|
229
|
+
* `ctx.email` stays inert until you set `PYLON_EMAIL_*` yourself — the
|
|
230
|
+
* shared auth key can never be used to send arbitrary mail.
|
|
231
|
+
*
|
|
232
|
+
* The runtime owns provider config + credentials; functions only
|
|
233
|
+
* supply the (to, subject, body) tuple. Failures are surfaced as
|
|
234
|
+
* thrown errors; on success the return is void.
|
|
235
|
+
*
|
|
236
|
+
* Use cases: invite emails, password-reset hand-offs, notifications,
|
|
237
|
+
* digest reports. NOT for marketing email — those should go through
|
|
238
|
+
* a dedicated bulk transport, not the transactional path.
|
|
239
|
+
*/
|
|
240
|
+
export interface EmailSender {
|
|
241
|
+
/** Send a plain-text email. `to` is a single address. */
|
|
242
|
+
send(to: string, subject: string, body: string): Promise<void>;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Server-side LLM client. Available on every ctx variant (query,
|
|
246
|
+
* mutation, action) because agent loops often run as queries —
|
|
247
|
+
* read tool args from the message, ship the response back.
|
|
248
|
+
*
|
|
249
|
+
* Provider is configured at the server boot (PYLON_LLM_PROVIDER +
|
|
250
|
+
* ANTHROPIC_API_KEY or OPENAI_API_KEY). The wire shape is Anthropic
|
|
251
|
+
* Messages — OpenAI calls translate at the transport boundary, so
|
|
252
|
+
* the same caller code works against either provider.
|
|
253
|
+
*
|
|
254
|
+
* The framework does NOT expose this surface to the browser; clients
|
|
255
|
+
* that need streaming should call POST /api/ai/stream directly.
|
|
256
|
+
* `ctx.llm.complete` is server-only on purpose — the API key never
|
|
257
|
+
* leaves the runtime process.
|
|
258
|
+
*/
|
|
259
|
+
export interface Llm {
|
|
260
|
+
/**
|
|
261
|
+
* Send a completion request to the configured LLM provider. The
|
|
262
|
+
* shape is Anthropic Messages: a list of {role, content} pairs
|
|
263
|
+
* where content is either a string or a list of content blocks
|
|
264
|
+
* (text, tool_use, tool_result). Returns the full response once
|
|
265
|
+
* the model finishes generating.
|
|
266
|
+
*
|
|
267
|
+
* For agent tool-use loops, inspect `response.stopReason` — when
|
|
268
|
+
* it's `"tool_use"`, append the assistant's content (which
|
|
269
|
+
* includes the `tool_use` blocks) plus your `tool_result`
|
|
270
|
+
* follow-ups to the message list and call again. Loop until
|
|
271
|
+
* `stopReason === "end_turn"`.
|
|
272
|
+
*
|
|
273
|
+
* Errors are thrown as standard Error objects with an `err.code`
|
|
274
|
+
* property set to one of: `LLM_NOT_CONFIGURED`, `MODEL_NOT_ALLOWED`,
|
|
275
|
+
* `MODEL_OVERRIDE_FORBIDDEN`, `PROVIDER_HTTP_<code>`,
|
|
276
|
+
* `PROVIDER_UNREACHABLE`, `INVALID_REQUEST`.
|
|
277
|
+
*/
|
|
278
|
+
complete(request: LlmCompleteRequest): Promise<LlmCompleteResponse>;
|
|
279
|
+
}
|
|
280
|
+
export interface LlmMessage {
|
|
281
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
282
|
+
content: string | LlmContentBlock[];
|
|
283
|
+
}
|
|
284
|
+
export type LlmContentBlock = {
|
|
285
|
+
type: "text";
|
|
286
|
+
text: string;
|
|
287
|
+
} | {
|
|
288
|
+
type: "tool_use";
|
|
289
|
+
id: string;
|
|
290
|
+
name: string;
|
|
291
|
+
input: Record<string, unknown>;
|
|
292
|
+
} | {
|
|
293
|
+
type: "tool_result";
|
|
294
|
+
tool_use_id: string;
|
|
295
|
+
content: string;
|
|
296
|
+
is_error?: boolean;
|
|
297
|
+
};
|
|
298
|
+
export interface LlmTool {
|
|
299
|
+
name: string;
|
|
300
|
+
description?: string;
|
|
301
|
+
/** JSON Schema object describing the tool's input shape. */
|
|
302
|
+
input_schema: Record<string, unknown>;
|
|
303
|
+
}
|
|
304
|
+
export interface LlmCompleteRequest {
|
|
305
|
+
/** Override the server's default model. Subject to
|
|
306
|
+
* PYLON_AI_MODELS_ALLOWED gating for non-admin callers. */
|
|
307
|
+
model?: string;
|
|
308
|
+
messages: LlmMessage[];
|
|
309
|
+
system?: string;
|
|
310
|
+
tools?: LlmTool[];
|
|
311
|
+
/** Defaults to 4096. */
|
|
312
|
+
max_tokens?: number;
|
|
313
|
+
temperature?: number;
|
|
314
|
+
}
|
|
315
|
+
export interface LlmCompleteResponse {
|
|
316
|
+
model: string;
|
|
317
|
+
content: LlmContentBlock[];
|
|
318
|
+
/** `end_turn` | `tool_use` | `max_tokens` | `stop_sequence` */
|
|
319
|
+
stop_reason: string;
|
|
320
|
+
usage: {
|
|
321
|
+
input_tokens: number;
|
|
322
|
+
output_tokens: number;
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Server-side OAuth connection registry. Apps declare connections
|
|
327
|
+
* via `defineConnection({...})` in `app.ts`; this surface lets
|
|
328
|
+
* actions fetch fresh access tokens (auto-refresh) and start the
|
|
329
|
+
* OAuth dance.
|
|
330
|
+
*
|
|
331
|
+
* Available on mutation + action ctx only — connections perform
|
|
332
|
+
* external I/O (token refresh, DB writes) that doesn't belong
|
|
333
|
+
* inside a reactive query.
|
|
334
|
+
*
|
|
335
|
+
* All ops require an authenticated caller (`ctx.auth.userId !==
|
|
336
|
+
* null`). Public functions must `ctx.auth.elevate({ admin: true,
|
|
337
|
+
* reason: "..." })` before reaching `ctx.connections.*`.
|
|
338
|
+
*/
|
|
339
|
+
export interface Connections {
|
|
340
|
+
/**
|
|
341
|
+
* Mint the URL the browser should navigate to so the user can
|
|
342
|
+
* link an external account. `name` matches a `defineConnection({...})`
|
|
343
|
+
* entry. `postRedirect` (optional) is where the browser lands
|
|
344
|
+
* after a successful callback — defaults to `/`.
|
|
345
|
+
*
|
|
346
|
+
* Throws `CONNECTIONS_NOT_CONFIGURED`, `CONNECTION_UNKNOWN`,
|
|
347
|
+
* `PROVIDER_NOT_CONFIGURED`, or `ENCRYPTION_REQUIRED` (refresh
|
|
348
|
+
* tokens are not allowed to land in plaintext).
|
|
349
|
+
*/
|
|
350
|
+
authorizeUrl(name: string, opts?: {
|
|
351
|
+
postRedirect?: string;
|
|
352
|
+
}): Promise<{
|
|
353
|
+
url: string;
|
|
354
|
+
}>;
|
|
355
|
+
/**
|
|
356
|
+
* Returns a fresh access token for `(ctx.auth.userId, name)`. If
|
|
357
|
+
* the stored token expires within 60s, the framework refreshes
|
|
358
|
+
* via the provider's refresh-token grant FIRST, persists the new
|
|
359
|
+
* token pair, then returns the new access token.
|
|
360
|
+
*
|
|
361
|
+
* Throws `CONNECTION_NOT_LINKED` when the user hasn't started
|
|
362
|
+
* the OAuth flow, `REFRESH_FAILED` when the provider rejects
|
|
363
|
+
* the refresh token (user must re-link).
|
|
364
|
+
*/
|
|
365
|
+
get(name: string): Promise<{
|
|
366
|
+
accessToken: string;
|
|
367
|
+
scope: string | null;
|
|
368
|
+
expiresAt: number | null;
|
|
369
|
+
}>;
|
|
370
|
+
/** List the signed-in user's linked connections. Token values
|
|
371
|
+
* are NOT included — call `get(name)` for those. */
|
|
372
|
+
list(): Promise<{
|
|
373
|
+
connections: Array<{
|
|
374
|
+
name: string;
|
|
375
|
+
provider: string;
|
|
376
|
+
scope: string | null;
|
|
377
|
+
expiresAt: number | null;
|
|
378
|
+
updatedAt: number;
|
|
379
|
+
}>;
|
|
380
|
+
}>;
|
|
381
|
+
/** Remove the stored connection. Provider-side revocation is
|
|
382
|
+
* the caller's responsibility — most providers expose a separate
|
|
383
|
+
* `/revoke` endpoint that this surface intentionally doesn't
|
|
384
|
+
* call (revoke vs unlink semantics differ per provider). */
|
|
385
|
+
disconnect(name: string): Promise<{
|
|
386
|
+
disconnected: boolean;
|
|
387
|
+
}>;
|
|
388
|
+
}
|
|
389
|
+
/** Options for `ctx.requireMember()`. */
|
|
390
|
+
export interface RequireMemberOptions {
|
|
391
|
+
/**
|
|
392
|
+
* Allowed role(s). The caller's membership role must be one of these.
|
|
393
|
+
* Omit to require ANY membership regardless of role.
|
|
394
|
+
*/
|
|
395
|
+
role?: string | string[];
|
|
396
|
+
/**
|
|
397
|
+
* The membership entity to check. Default `"OrgMember"` — the same entity
|
|
398
|
+
* the framework's org/tenant machinery uses. Override for a custom model.
|
|
399
|
+
*/
|
|
400
|
+
entity?: string;
|
|
401
|
+
/** Field on the membership entity holding the org/tenant id. Default `"orgId"`. */
|
|
402
|
+
orgField?: string;
|
|
403
|
+
/** Field holding the user id. Default `"userId"`. */
|
|
404
|
+
userField?: string;
|
|
405
|
+
/** Field holding the role. Default `"role"`. */
|
|
406
|
+
roleField?: string;
|
|
407
|
+
}
|
|
408
|
+
/** The membership row returned by `ctx.requireMember()`. */
|
|
409
|
+
export type MemberRow = Record<string, unknown> & {
|
|
410
|
+
role?: string;
|
|
411
|
+
};
|
|
412
|
+
/**
|
|
413
|
+
* Assert the caller is a member of `orgId` (optionally with one of `role`),
|
|
414
|
+
* returning the membership row. Throws a typed error otherwise:
|
|
415
|
+
* `UNAUTHENTICATED` (no signed-in user), `MISSING_ORG` (no orgId), or
|
|
416
|
+
* `FORBIDDEN` (not a member / wrong role).
|
|
417
|
+
*
|
|
418
|
+
* This is the authoritative authorization gate for org-scoped writes —
|
|
419
|
+
* actions + mutations BYPASS entity read policies, so a function that trusts
|
|
420
|
+
* an attacker-supplied `orgId`/`projectId` is an IDOR unless it re-checks
|
|
421
|
+
* membership. `requireMember` makes the safe path the default path.
|
|
422
|
+
*
|
|
423
|
+
* ```ts
|
|
424
|
+
* export default mutation({
|
|
425
|
+
* args: { orgId: v.id("Organization"), name: v.string() },
|
|
426
|
+
* async handler(ctx, args) {
|
|
427
|
+
* await ctx.requireMember(args.orgId, { role: ["owner", "admin"] });
|
|
428
|
+
* // …safe to mutate org-scoped data now…
|
|
429
|
+
* },
|
|
430
|
+
* });
|
|
431
|
+
* ```
|
|
432
|
+
*
|
|
433
|
+
* The membership entity must let the caller read their OWN membership row
|
|
434
|
+
* (the standard `auth.userId == data.userId` read policy) — the check runs
|
|
435
|
+
* with the caller's identity.
|
|
436
|
+
*/
|
|
437
|
+
export type RequireMember = (orgId: string, opts?: RequireMemberOptions) => Promise<MemberRow>;
|
|
438
|
+
/** Context for query handlers (read-only).
|
|
439
|
+
*
|
|
440
|
+
* NOTE: `ctx.llm` is NOT exposed here. Queries are reactive: a
|
|
441
|
+
* subscribed query re-runs whenever its `ctx.db.*` reads change.
|
|
442
|
+
* Calling a stochastic, paid LLM from a query would (a) silently
|
|
443
|
+
* burn the framework's API key on every dep invalidation, and
|
|
444
|
+
* (b) violate the reactive purity contract (same inputs → same
|
|
445
|
+
* outputs). LLM calls belong in mutations (transactional) or
|
|
446
|
+
* actions (external I/O). */
|
|
447
|
+
export interface QueryCtx<R extends AuthRequirement = "optional"> {
|
|
448
|
+
db: DbReader;
|
|
449
|
+
auth: AuthInfo<R>;
|
|
450
|
+
/** Environment variables / secrets. */
|
|
451
|
+
env: Record<string, string>;
|
|
452
|
+
/** Assert org membership (optionally a role) — see {@link RequireMember}. */
|
|
453
|
+
requireMember: RequireMember;
|
|
454
|
+
}
|
|
455
|
+
/** Context for mutation handlers (read + write, transactional). */
|
|
456
|
+
export interface MutationCtx<R extends AuthRequirement = "optional"> {
|
|
457
|
+
db: DbWriter;
|
|
458
|
+
auth: AuthInfo<R>;
|
|
459
|
+
stream: Stream;
|
|
460
|
+
scheduler: Scheduler;
|
|
461
|
+
/** Environment variables / secrets. */
|
|
462
|
+
env: Record<string, string>;
|
|
463
|
+
/** Provider-abstracted LLM client. */
|
|
464
|
+
llm: Llm;
|
|
465
|
+
/** Per-user OAuth connection registry. */
|
|
466
|
+
connections: Connections;
|
|
467
|
+
/** Create a typed error that triggers rollback. */
|
|
468
|
+
error(code: string, message: string): Error;
|
|
469
|
+
/** Assert org membership (optionally a role) — see {@link RequireMember}. */
|
|
470
|
+
requireMember: RequireMember;
|
|
471
|
+
}
|
|
472
|
+
/** Context for action handlers (external I/O, non-transactional). */
|
|
473
|
+
export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
474
|
+
auth: AuthInfo<R>;
|
|
475
|
+
stream: Stream;
|
|
476
|
+
scheduler: Scheduler;
|
|
477
|
+
/** Send transactional email via the runtime's configured provider. */
|
|
478
|
+
email: EmailSender;
|
|
479
|
+
/** Provider-abstracted LLM client. */
|
|
480
|
+
llm: Llm;
|
|
481
|
+
/** Per-user OAuth connection registry. */
|
|
482
|
+
connections: Connections;
|
|
483
|
+
/** Environment variables / secrets. */
|
|
484
|
+
env: Record<string, string>;
|
|
485
|
+
/** Run a registered query within its own read transaction. */
|
|
486
|
+
runQuery<T = unknown>(fnName: string, args: Record<string, unknown>): Promise<T>;
|
|
487
|
+
/** Run a registered mutation within its own write transaction. */
|
|
488
|
+
runMutation<T = unknown>(fnName: string, args: Record<string, unknown>): Promise<T>;
|
|
489
|
+
/** Create a typed error. */
|
|
490
|
+
error(code: string, message: string): Error;
|
|
491
|
+
/** Assert org membership (optionally a role) — see {@link RequireMember}. */
|
|
492
|
+
requireMember: RequireMember;
|
|
493
|
+
/**
|
|
494
|
+
* HTTP request metadata — present only when the action was invoked via
|
|
495
|
+
* a `defineRoute` HTTP binding. Missing when the action is called from
|
|
496
|
+
* another action (`ctx.runAction`), a job, or the function dashboard.
|
|
497
|
+
*
|
|
498
|
+
* Use this to verify webhook signatures (Stripe, GitHub, Slack) that
|
|
499
|
+
* require the raw request body — `rawBody` is the exact bytes the
|
|
500
|
+
* signer signed, NOT the parsed JSON.
|
|
501
|
+
*
|
|
502
|
+
* ```ts
|
|
503
|
+
* export default action({
|
|
504
|
+
* async handler(ctx) {
|
|
505
|
+
* const sig = ctx.request?.headers["stripe-signature"];
|
|
506
|
+
* stripe.webhooks.constructEvent(ctx.request!.rawBody, sig!, secret);
|
|
507
|
+
* },
|
|
508
|
+
* });
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
511
|
+
request?: RequestInfo;
|
|
512
|
+
}
|
|
513
|
+
/** HTTP request metadata available on an action's ctx when invoked via an
|
|
514
|
+
* HTTP route binding. Header names are lowercased. */
|
|
515
|
+
export interface RequestInfo {
|
|
516
|
+
method: string;
|
|
517
|
+
path: string;
|
|
518
|
+
headers: Record<string, string>;
|
|
519
|
+
rawBody: string;
|
|
520
|
+
}
|
|
521
|
+
export type FnType = "query" | "mutation" | "action";
|
|
522
|
+
export interface FnDefinition<TArgs = unknown, TReturn = unknown> {
|
|
523
|
+
type: FnType;
|
|
524
|
+
args?: Record<string, Validator>;
|
|
525
|
+
handler: (ctx: any, args: TArgs) => Promise<TReturn>;
|
|
526
|
+
/**
|
|
527
|
+
* When true, this function is reachable only via `ctx.runQuery()` /
|
|
528
|
+
* `ctx.runMutation()` / `ctx.runAction()` from another function —
|
|
529
|
+
* the public `/api/fn/<name>` endpoint refuses external calls.
|
|
530
|
+
* The router enforces this; the runtime treats internal == external
|
|
531
|
+
* for execution.
|
|
532
|
+
*/
|
|
533
|
+
internal?: boolean;
|
|
534
|
+
/**
|
|
535
|
+
* Auth requirement enforced by the runtime before the handler is
|
|
536
|
+
* invoked. Defaults to `"user"` — every function is signed-in only
|
|
537
|
+
* unless explicitly opted out via `auth: "public"`. See [`AuthMode`].
|
|
538
|
+
*/
|
|
539
|
+
auth?: AuthMode;
|
|
540
|
+
/**
|
|
541
|
+
* Max wall-clock seconds this function may run before the runtime
|
|
542
|
+
* recycles its worker. Defaults to `PYLON_FN_CALL_TIMEOUT` (30s).
|
|
543
|
+
* Raise for legitimately long-running work; also lifts the wedge
|
|
544
|
+
* backstop while the call is in flight. See the `timeout` option docs.
|
|
545
|
+
*/
|
|
546
|
+
timeout?: number;
|
|
547
|
+
}
|
|
548
|
+
export interface Validator {
|
|
549
|
+
type: string;
|
|
550
|
+
optional?: boolean;
|
|
551
|
+
/** For v.id("tableName") */
|
|
552
|
+
table?: string;
|
|
553
|
+
/** For v.array(v.string()) */
|
|
554
|
+
items?: Validator;
|
|
555
|
+
/** For v.object({...}) */
|
|
556
|
+
fields?: Record<string, Validator>;
|
|
557
|
+
/** For v.union(...) */
|
|
558
|
+
variants?: Validator[];
|
|
559
|
+
/** For v.literal("value") */
|
|
560
|
+
value?: unknown;
|
|
561
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument validators for function definitions.
|
|
3
|
+
*
|
|
4
|
+
* These serve double duty:
|
|
5
|
+
* 1. Runtime validation — reject bad input before the handler runs.
|
|
6
|
+
* 2. Type inference — TypeScript infers handler arg types from validators.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { mutation, v } from "@pylonsync/functions";
|
|
11
|
+
*
|
|
12
|
+
* export default mutation({
|
|
13
|
+
* args: {
|
|
14
|
+
* name: v.string(),
|
|
15
|
+
* age: v.optional(v.number()),
|
|
16
|
+
* tags: v.array(v.string()),
|
|
17
|
+
* },
|
|
18
|
+
* async handler(ctx, args) {
|
|
19
|
+
* // args is typed as { name: string, age?: number, tags: string[] }
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import type { Validator } from "./types";
|
|
25
|
+
export declare const v: {
|
|
26
|
+
/** String value. */
|
|
27
|
+
string: () => Validator;
|
|
28
|
+
/** Number (float64). Same as `v.float()`. */
|
|
29
|
+
number: () => Validator;
|
|
30
|
+
/**
|
|
31
|
+
* 64-bit float. Alias for `v.number()` so the validator API matches the
|
|
32
|
+
* schema DSL (which uses `field.float()`). Prefer this in new code.
|
|
33
|
+
*/
|
|
34
|
+
float: () => Validator;
|
|
35
|
+
/** Integer. */
|
|
36
|
+
int: () => Validator;
|
|
37
|
+
/** Boolean. Same as `v.bool()`. */
|
|
38
|
+
boolean: () => Validator;
|
|
39
|
+
/**
|
|
40
|
+
* Boolean. Alias for `v.boolean()` so the validator API matches the
|
|
41
|
+
* schema DSL (which uses `field.bool()`). Prefer this in new code.
|
|
42
|
+
*/
|
|
43
|
+
bool: () => Validator;
|
|
44
|
+
/**
|
|
45
|
+
* ISO-8601 datetime string. Validates the shape of a string value; the
|
|
46
|
+
* stored column type comes from the schema (`field.datetime()`).
|
|
47
|
+
*/
|
|
48
|
+
datetime: () => Validator;
|
|
49
|
+
/**
|
|
50
|
+
* Richtext string. Same runtime validation as `v.string()`; named
|
|
51
|
+
* explicitly so server functions read as the matching schema type.
|
|
52
|
+
*/
|
|
53
|
+
richtext: () => Validator;
|
|
54
|
+
/** ID reference to another entity. */
|
|
55
|
+
id: (table: string) => Validator;
|
|
56
|
+
/** Null value. */
|
|
57
|
+
null: () => Validator;
|
|
58
|
+
/** Array of values. */
|
|
59
|
+
array: (items: Validator) => Validator;
|
|
60
|
+
/** Object with typed fields. */
|
|
61
|
+
object: (fields: Record<string, Validator>) => Validator;
|
|
62
|
+
/** Optional value (may be omitted). */
|
|
63
|
+
optional: (inner: Validator) => Validator;
|
|
64
|
+
/** Union of multiple types. */
|
|
65
|
+
union: (...variants: Validator[]) => Validator;
|
|
66
|
+
/** Exact literal value. */
|
|
67
|
+
literal: (value: string | number | boolean) => Validator;
|
|
68
|
+
/** Any valid JSON value. */
|
|
69
|
+
any: () => Validator;
|
|
70
|
+
};
|
|
71
|
+
export declare function validateArgs(args: unknown, schema: Record<string, Validator>): {
|
|
72
|
+
valid: boolean;
|
|
73
|
+
errors: string[];
|
|
74
|
+
};
|
package/package.json
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pylonsync/functions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.294",
|
|
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",
|
|
7
|
-
"types": "
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./src/index.ts"
|
|
12
12
|
},
|
|
13
|
-
"./runtime":
|
|
14
|
-
|
|
13
|
+
"./runtime": {
|
|
14
|
+
"types": "./dist/runtime.d.ts",
|
|
15
|
+
"default": "./src/runtime.ts"
|
|
16
|
+
},
|
|
17
|
+
"./client-bundler": {
|
|
18
|
+
"types": "./dist/ssr-client-bundler.d.ts",
|
|
19
|
+
"default": "./src/ssr-client-bundler.ts"
|
|
20
|
+
}
|
|
15
21
|
},
|
|
16
22
|
"bin": {
|
|
17
23
|
"pylon-functions-runtime": "src/runtime.ts"
|
|
18
24
|
},
|
|
19
25
|
"files": [
|
|
20
26
|
"src",
|
|
27
|
+
"dist",
|
|
21
28
|
"README.md"
|
|
22
29
|
],
|
|
23
30
|
"scripts": {
|
|
24
31
|
"typecheck": "tsc --noEmit",
|
|
25
|
-
"build": "tsc",
|
|
26
|
-
"test": "bun test"
|
|
32
|
+
"build": "tsc -p tsconfig.build.json",
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"prepack": "bun run build"
|
|
27
35
|
},
|
|
28
36
|
"keywords": [
|
|
29
37
|
"pylon",
|