@pylonsync/functions 0.3.292 → 0.3.293

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.
@@ -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.292",
3
+ "version": "0.3.293",
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": "src/index.ts",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
11
11
  "default": "./src/index.ts"
12
12
  },
13
- "./runtime": "./src/runtime.ts",
14
- "./client-bundler": "./src/ssr-client-bundler.ts"
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",