@pylonsync/functions 0.4.0 → 0.4.1

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/runtime.d.ts CHANGED
@@ -16,14 +16,6 @@
16
16
  */
17
17
  import type { DbReader, DbWriter, Llm, Rooms } from "./types";
18
18
  export declare function buildDbReader(callId: string, ssrRead?: boolean): DbReader;
19
- /**
20
- * Query-function caller for SSR `serverData.fn(name, args)`. Sends the same
21
- * `run_fn` frame `ctx.runQuery` uses, on the render's call_id — the host's
22
- * render loop executes the query with the PAGE's auth context (anonymous on
23
- * public pages) and rejects anything that isn't a query. Legacy rpc() (no
24
- * op_id) is correct here: concurrent calls queue settle-chained on the one
25
- * call_id, matching how ctx.runQuery behaves inside a function.
26
- */
27
19
  export declare function buildSsrFnCaller(callId: string): (name: string, args?: Record<string, unknown>) => Promise<unknown>;
28
20
  export declare function buildDbWriter(callId: string): DbWriter;
29
21
  /**
package/dist/types.d.ts CHANGED
@@ -550,6 +550,38 @@ export type MemberRow = Record<string, unknown> & {
550
550
  * with the caller's identity.
551
551
  */
552
552
  export type RequireMember = (orgId: string, opts?: RequireMemberOptions) => Promise<MemberRow>;
553
+ /**
554
+ * Signed file-download URLs. `GET /api/files/<id>` only serves a file to its
555
+ * owner (or an unscoped admin) — `signedUrl` is the supported way to
556
+ * authorize a cross-user read: the host mints a short-lived HMAC-signed
557
+ * path (`/api/files/<id>?sig=...&exp=...`) that the GET handler honors as
558
+ * an alternative to the owner check, anonymously fetchable (works in
559
+ * `<img src>` and download links).
560
+ *
561
+ * WHO gets a URL is the calling function's responsibility — wrap the mint
562
+ * in a membership-gated function so authorization stays app-policy-driven:
563
+ *
564
+ * ```ts
565
+ * export default query({
566
+ * args: { eventId: v.id("Event"), fileId: v.string() },
567
+ * async handler(ctx, args) {
568
+ * const event = await ctx.db.get("Event", args.eventId);
569
+ * await ctx.requireMember(event.orgId, { role: "organizer" });
570
+ * return { url: await ctx.files.signedUrl(args.fileId) };
571
+ * },
572
+ * });
573
+ * ```
574
+ */
575
+ export interface Files {
576
+ /**
577
+ * Mint a signed download path for a file id. `ttlSecs` defaults to 300
578
+ * and is capped by the host (24h) — a signed URL is a bearer capability,
579
+ * so keep lifetimes short.
580
+ */
581
+ signedUrl(fileId: string, opts?: {
582
+ ttlSecs?: number;
583
+ }): Promise<string>;
584
+ }
553
585
  /** Context for query handlers (read-only).
554
586
  *
555
587
  * NOTE: `ctx.llm` is NOT exposed here. Queries are reactive: a
@@ -573,6 +605,8 @@ export interface QueryCtx<R extends AuthRequirement = "optional"> {
573
605
  error(code: string, message: string): Error;
574
606
  /** Assert org membership (optionally a role) — see {@link RequireMember}. */
575
607
  requireMember: RequireMember;
608
+ /** Signed file-download URLs — see {@link Files}. */
609
+ files: Files;
576
610
  }
577
611
  /** Context for mutation handlers (read + write, transactional). */
578
612
  export interface MutationCtx<R extends AuthRequirement = "optional"> {
@@ -588,6 +622,8 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
588
622
  rooms: Rooms;
589
623
  /** Per-user OAuth connection registry. */
590
624
  connections: Connections;
625
+ /** Signed file-download URLs — see {@link Files}. */
626
+ files: Files;
591
627
  /** Create a typed error that triggers rollback. */
592
628
  error(code: string, message: string): Error;
593
629
  /** Assert org membership (optionally a role) — see {@link RequireMember}. */
@@ -608,6 +644,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
608
644
  connections: Connections;
609
645
  /** Environment variables / secrets. */
610
646
  env: Record<string, string>;
647
+ /** Signed file-download URLs — see {@link Files}. */
648
+ files: Files;
611
649
  /** Run a registered query within its own read transaction. */
612
650
  runQuery<T = unknown>(fnName: string, args: Record<string, unknown>): Promise<T>;
613
651
  /** Run a registered mutation within its own write transaction. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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/runtime.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  EmailAttachment,
22
22
  EmailOptions,
23
23
  EmailSender,
24
+ Files,
24
25
  Stream,
25
26
  Scheduler,
26
27
  Llm,
@@ -518,6 +519,19 @@ export function buildDbReader(callId: string, ssrRead = false): DbReader {
518
519
  * op_id) is correct here: concurrent calls queue settle-chained on the one
519
520
  * call_id, matching how ctx.runQuery behaves inside a function.
520
521
  */
522
+ /** `ctx.files` — signed download URLs, minted host-side. */
523
+ function buildFiles(callId: string): Files {
524
+ return {
525
+ async signedUrl(fileId, opts) {
526
+ return rpc(callId, {
527
+ type: "sign_file_url",
528
+ file_id: fileId,
529
+ ttl_secs: opts?.ttlSecs,
530
+ }) as Promise<string>;
531
+ },
532
+ };
533
+ }
534
+
521
535
  export function buildSsrFnCaller(
522
536
  callId: string,
523
537
  ): (name: string, args?: Record<string, unknown>) => Promise<unknown> {
@@ -991,6 +1005,7 @@ function buildActionCtx(
991
1005
  (err as any).code = code;
992
1006
  return err;
993
1007
  },
1008
+ files: buildFiles(callId),
994
1009
  // Actions have no ctx.db; read membership via the built-in internal query.
995
1010
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
996
1011
  rpc(callId, {
@@ -1133,6 +1148,7 @@ async function handleCall(msg: CallMessage): Promise<void> {
1133
1148
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
1134
1149
  reader.query(entity, { ...filter, $limit: 1 }),
1135
1150
  ),
1151
+ files: buildFiles(msg.call_id),
1136
1152
  };
1137
1153
  break;
1138
1154
  }
@@ -1155,6 +1171,7 @@ async function handleCall(msg: CallMessage): Promise<void> {
1155
1171
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
1156
1172
  writer.query(entity, { ...filter, $limit: 1 }),
1157
1173
  ),
1174
+ files: buildFiles(msg.call_id),
1158
1175
  };
1159
1176
  break;
1160
1177
  }
package/src/types.ts CHANGED
@@ -644,6 +644,37 @@ export type RequireMember = (
644
644
  opts?: RequireMemberOptions,
645
645
  ) => Promise<MemberRow>;
646
646
 
647
+ /**
648
+ * Signed file-download URLs. `GET /api/files/<id>` only serves a file to its
649
+ * owner (or an unscoped admin) — `signedUrl` is the supported way to
650
+ * authorize a cross-user read: the host mints a short-lived HMAC-signed
651
+ * path (`/api/files/<id>?sig=...&exp=...`) that the GET handler honors as
652
+ * an alternative to the owner check, anonymously fetchable (works in
653
+ * `<img src>` and download links).
654
+ *
655
+ * WHO gets a URL is the calling function's responsibility — wrap the mint
656
+ * in a membership-gated function so authorization stays app-policy-driven:
657
+ *
658
+ * ```ts
659
+ * export default query({
660
+ * args: { eventId: v.id("Event"), fileId: v.string() },
661
+ * async handler(ctx, args) {
662
+ * const event = await ctx.db.get("Event", args.eventId);
663
+ * await ctx.requireMember(event.orgId, { role: "organizer" });
664
+ * return { url: await ctx.files.signedUrl(args.fileId) };
665
+ * },
666
+ * });
667
+ * ```
668
+ */
669
+ export interface Files {
670
+ /**
671
+ * Mint a signed download path for a file id. `ttlSecs` defaults to 300
672
+ * and is capped by the host (24h) — a signed URL is a bearer capability,
673
+ * so keep lifetimes short.
674
+ */
675
+ signedUrl(fileId: string, opts?: { ttlSecs?: number }): Promise<string>;
676
+ }
677
+
647
678
  /** Context for query handlers (read-only).
648
679
  *
649
680
  * NOTE: `ctx.llm` is NOT exposed here. Queries are reactive: a
@@ -667,6 +698,8 @@ export interface QueryCtx<R extends AuthRequirement = "optional"> {
667
698
  error(code: string, message: string): Error;
668
699
  /** Assert org membership (optionally a role) — see {@link RequireMember}. */
669
700
  requireMember: RequireMember;
701
+ /** Signed file-download URLs — see {@link Files}. */
702
+ files: Files;
670
703
  }
671
704
 
672
705
  /** Context for mutation handlers (read + write, transactional). */
@@ -683,6 +716,8 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
683
716
  rooms: Rooms;
684
717
  /** Per-user OAuth connection registry. */
685
718
  connections: Connections;
719
+ /** Signed file-download URLs — see {@link Files}. */
720
+ files: Files;
686
721
  /** Create a typed error that triggers rollback. */
687
722
  error(code: string, message: string): Error;
688
723
  /** Assert org membership (optionally a role) — see {@link RequireMember}. */
@@ -704,6 +739,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
704
739
  connections: Connections;
705
740
  /** Environment variables / secrets. */
706
741
  env: Record<string, string>;
742
+ /** Signed file-download URLs — see {@link Files}. */
743
+ files: Files;
707
744
  /** Run a registered query within its own read transaction. */
708
745
  runQuery<T = unknown>(
709
746
  fnName: string,