@pylonsync/functions 0.3.26 → 0.3.28

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.26",
3
+ "version": "0.3.28",
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
@@ -15,16 +15,32 @@ import type {
15
15
  interface QueryDef<TArgs, TReturn> {
16
16
  args?: Record<string, Validator>;
17
17
  handler: (ctx: QueryCtx, args: TArgs) => Promise<TReturn>;
18
+ /**
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.
23
+ *
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).
28
+ */
29
+ internal?: boolean;
18
30
  }
19
31
 
20
32
  interface MutationDef<TArgs, TReturn> {
21
33
  args?: Record<string, Validator>;
22
34
  handler: (ctx: MutationCtx, args: TArgs) => Promise<TReturn>;
35
+ /** See QueryDef.internal — applies the same way to mutations. */
36
+ internal?: boolean;
23
37
  }
24
38
 
25
39
  interface ActionDef<TArgs, TReturn> {
26
40
  args?: Record<string, Validator>;
27
41
  handler: (ctx: ActionCtx, args: TArgs) => Promise<TReturn>;
42
+ /** See QueryDef.internal — applies the same way to actions. */
43
+ internal?: boolean;
28
44
  }
29
45
 
30
46
  /**
@@ -49,7 +65,12 @@ interface ActionDef<TArgs, TReturn> {
49
65
  export function query<TArgs = Record<string, unknown>, TReturn = unknown>(
50
66
  def: QueryDef<TArgs, TReturn>
51
67
  ): FnDefinition<TArgs, TReturn> {
52
- return { type: "query", args: def.args, handler: def.handler };
68
+ return {
69
+ type: "query",
70
+ args: def.args,
71
+ handler: def.handler,
72
+ internal: def.internal,
73
+ };
53
74
  }
54
75
 
55
76
  /**
@@ -78,7 +99,12 @@ export function query<TArgs = Record<string, unknown>, TReturn = unknown>(
78
99
  export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
79
100
  def: MutationDef<TArgs, TReturn>
80
101
  ): FnDefinition<TArgs, TReturn> {
81
- return { type: "mutation", args: def.args, handler: def.handler };
102
+ return {
103
+ type: "mutation",
104
+ args: def.args,
105
+ handler: def.handler,
106
+ internal: def.internal,
107
+ };
82
108
  }
83
109
 
84
110
  /**
@@ -105,5 +131,10 @@ export function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(
105
131
  export function action<TArgs = Record<string, unknown>, TReturn = unknown>(
106
132
  def: ActionDef<TArgs, TReturn>
107
133
  ): FnDefinition<TArgs, TReturn> {
108
- return { type: "action", args: def.args, handler: def.handler };
134
+ return {
135
+ type: "action",
136
+ args: def.args,
137
+ handler: def.handler,
138
+ internal: def.internal,
139
+ };
109
140
  }
package/src/runtime.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  import type {
19
19
  DbReader,
20
20
  DbWriter,
21
+ EmailSender,
21
22
  Stream,
22
23
  Scheduler,
23
24
  QueryCtx,
@@ -441,11 +442,33 @@ function buildScheduler(callId: string): Scheduler {
441
442
  };
442
443
  }
443
444
 
445
+ /**
446
+ * Build the email sender that round-trips through the host runtime.
447
+ *
448
+ * Each `send` emits a `send_email` protocol message; the runtime
449
+ * forwards to whatever transport PYLON_EMAIL_PROVIDER points at and
450
+ * replies success or error. Errors arrive as thrown exceptions on
451
+ * the action's await, just like every other RPC. No silent failures.
452
+ */
453
+ function buildEmail(callId: string): EmailSender {
454
+ return {
455
+ async send(to, subject, body) {
456
+ await rpc(callId, {
457
+ type: "send_email",
458
+ to,
459
+ subject,
460
+ body,
461
+ });
462
+ },
463
+ };
464
+ }
465
+
444
466
  function buildActionCtx(
445
467
  callId: string,
446
468
  auth: AuthInfo,
447
469
  stream: Stream,
448
470
  scheduler: Scheduler,
471
+ email: EmailSender,
449
472
  request?: unknown
450
473
  ): ActionCtx {
451
474
  // The host sends `request` as snake_case JSON (`raw_body`); normalize it
@@ -465,6 +488,7 @@ function buildActionCtx(
465
488
  auth,
466
489
  stream,
467
490
  scheduler,
491
+ email,
468
492
  env: process.env as Record<string, string>,
469
493
  async runQuery(fnName, args) {
470
494
  return rpc(callId, {
@@ -544,6 +568,7 @@ async function handleCall(msg: CallMessage): Promise<void> {
544
568
 
545
569
  const stream = buildStream(msg.call_id);
546
570
  const scheduler = buildScheduler(msg.call_id);
571
+ const email = buildEmail(msg.call_id);
547
572
 
548
573
  // Normalize the Rust-side auth envelope (snake_case) to the camelCase
549
574
  // shape that AuthInfo documents. Handlers read `ctx.auth.userId`; the
@@ -597,6 +622,7 @@ async function handleCall(msg: CallMessage): Promise<void> {
597
622
  auth,
598
623
  stream,
599
624
  scheduler,
625
+ email,
600
626
  (msg as unknown as { request?: unknown }).request,
601
627
  );
602
628
  break;
@@ -689,6 +715,11 @@ async function main() {
689
715
  name,
690
716
  fn_type: def.type,
691
717
  args_schema: def.args || null,
718
+ // Whether the function is callable only via runQuery/runMutation/
719
+ // runAction from another function. The Rust router refuses /api/fn
720
+ // requests for internal fns; the Bun runtime here doesn't gate
721
+ // (nested calls go through the same dispatcher).
722
+ internal: def.internal === true,
692
723
  }));
693
724
  send({ type: "ready", functions });
694
725
 
package/src/types.ts CHANGED
@@ -171,6 +171,27 @@ export interface Scheduler {
171
171
  cancel(scheduleId: string): Promise<void>;
172
172
  }
173
173
 
174
+ /**
175
+ * Transactional email transport.
176
+ *
177
+ * Sends through whatever provider the runtime is configured for
178
+ * (PYLON_EMAIL_PROVIDER env var → SendGrid / Resend / Stack0 / SMTP /
179
+ * webhook). Available on action ctx only — sending email is external
180
+ * I/O, not allowed in mutation transactions.
181
+ *
182
+ * The runtime owns provider config + credentials; functions only
183
+ * supply the (to, subject, body) tuple. Failures are surfaced as
184
+ * thrown errors; on success the return is void.
185
+ *
186
+ * Use cases: invite emails, password-reset hand-offs, notifications,
187
+ * digest reports. NOT for marketing email — those should go through
188
+ * a dedicated bulk transport, not the transactional path.
189
+ */
190
+ export interface EmailSender {
191
+ /** Send a plain-text email. `to` is a single address. */
192
+ send(to: string, subject: string, body: string): Promise<void>;
193
+ }
194
+
174
195
  // ---------------------------------------------------------------------------
175
196
  // Context objects — what handlers receive
176
197
  // ---------------------------------------------------------------------------
@@ -200,6 +221,8 @@ export interface ActionCtx {
200
221
  auth: AuthInfo;
201
222
  stream: Stream;
202
223
  scheduler: Scheduler;
224
+ /** Send transactional email via the runtime's configured provider. */
225
+ email: EmailSender;
203
226
  /** Environment variables / secrets. */
204
227
  env: Record<string, string>;
205
228
  /** Run a registered query within its own read transaction. */
@@ -254,6 +277,14 @@ export interface FnDefinition<TArgs = unknown, TReturn = unknown> {
254
277
  type: FnType;
255
278
  args?: Record<string, Validator>;
256
279
  handler: (ctx: any, args: TArgs) => Promise<TReturn>;
280
+ /**
281
+ * When true, this function is reachable only via `ctx.runQuery()` /
282
+ * `ctx.runMutation()` / `ctx.runAction()` from another function —
283
+ * the public `/api/fn/<name>` endpoint refuses external calls.
284
+ * The router enforces this; the runtime treats internal == external
285
+ * for execution.
286
+ */
287
+ internal?: boolean;
257
288
  }
258
289
 
259
290
  // ---------------------------------------------------------------------------