@pylonsync/functions 0.22.7 → 0.22.9

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/index.d.ts CHANGED
@@ -27,4 +27,4 @@ export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunne
27
27
  export { resetDb, installTestIsolation } from "./testing";
28
28
  export { slugifyName, availableSlug } from "./slugify";
29
29
  export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
30
- export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, } from "./types";
30
+ export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, Files, ActionFiles, StoreFileOptions, StoredFile, } from "./types";
package/dist/types.d.ts CHANGED
@@ -890,6 +890,44 @@ export interface Files {
890
890
  ttlSecs?: number;
891
891
  }): Promise<string>;
892
892
  }
893
+ /** Options for {@link ActionFiles.store}. */
894
+ export interface StoreFileOptions {
895
+ /** File name, 1-255 characters, no path separators. */
896
+ name: string;
897
+ /** Defaults to `application/octet-stream`. */
898
+ contentType?: string;
899
+ /** Readable by anyone, signed in or not. Defaults to false. */
900
+ public?: boolean;
901
+ }
902
+ /** A file written by {@link ActionFiles.store}. */
903
+ export interface StoredFile {
904
+ id: string;
905
+ /** Where the file is served: `/api/files/<id>` or the storage CDN URL. */
906
+ url: string;
907
+ size: number;
908
+ }
909
+ /**
910
+ * `ctx.files` in an action: the read-side {@link Files} plus writes to the
911
+ * app's file storage (the backend `/api/files` uses). Writes run only in
912
+ * actions: a stored or deleted file cannot roll back with a query or
913
+ * mutation.
914
+ *
915
+ * A stored file is owned by the calling user and tenant, as if that user
916
+ * had uploaded it. With no user (a scheduled job, a public webhook) the
917
+ * file is system-owned: readable through {@link Files.signedUrl}, by an
918
+ * unscoped admin, or by anyone when `public` is set.
919
+ */
920
+ export interface ActionFiles extends Files {
921
+ /**
922
+ * Store bytes as a new file. At most 25 MiB, or `PYLON_MAX_UPLOAD_BYTES`
923
+ * when that is lower.
924
+ */
925
+ store(data: Uint8Array | ArrayBuffer | string, opts: StoreFileOptions): Promise<StoredFile>;
926
+ /** Delete a stored file. `deleted` is false when it did not exist. */
927
+ delete(fileId: string): Promise<{
928
+ deleted: boolean;
929
+ }>;
930
+ }
893
931
  /**
894
932
  * Shard tickets: short-lived, signed permission to join one realtime shard,
895
933
  * with claims the shard's authorization hooks read without a database
@@ -1312,7 +1350,7 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
1312
1350
  /** Environment variables / secrets. */
1313
1351
  env: Record<string, string>;
1314
1352
  /** Signed file-download URLs — see {@link Files}. */
1315
- files: Files;
1353
+ files: ActionFiles;
1316
1354
  /** Shard tickets, reads, and start/stop — see {@link Shards}. */
1317
1355
  shards: Shards;
1318
1356
  /** Run a registered query within its own read transaction. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.22.7",
3
+ "version": "0.22.9",
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/index.ts CHANGED
@@ -114,4 +114,9 @@ export type {
114
114
  DomainContact,
115
115
  RegisterDomainOptions,
116
116
  RegisteredDomainResult,
117
+ // File storage from functions.
118
+ Files,
119
+ ActionFiles,
120
+ StoreFileOptions,
121
+ StoredFile,
117
122
  } from "./types";
package/src/runtime.ts CHANGED
@@ -22,6 +22,8 @@ import type {
22
22
  EmailOptions,
23
23
  EmailSender,
24
24
  Files,
25
+ ActionFiles,
26
+ StoredFile,
25
27
  Shards,
26
28
  ShardInfo,
27
29
  ShardTransfer,
@@ -640,6 +642,56 @@ function buildFiles(callId: string): Files {
640
642
  };
641
643
  }
642
644
 
645
+ /** `ctx.files` in an action: {@link buildFiles} plus store and delete. */
646
+ function buildActionFiles(callId: string): ActionFiles {
647
+ return {
648
+ ...buildFiles(callId),
649
+ async store(data, opts) {
650
+ const size =
651
+ typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
652
+ // Refused here, before encoding: a larger message would exceed the
653
+ // runner's frame limit and never reach the host's check.
654
+ if (size > FILES_STORE_MAX_BYTES) {
655
+ const err = new Error(
656
+ `ctx.files.store accepts at most ${FILES_STORE_MAX_BYTES} bytes (got ${size})`,
657
+ );
658
+ (err as { code?: string }).code = "FILE_TOO_LARGE";
659
+ throw err;
660
+ }
661
+ return rpc(callId, {
662
+ type: "files_op",
663
+ op: "store",
664
+ name: opts.name,
665
+ content_type: opts.contentType ?? null,
666
+ public: opts.public ?? false,
667
+ data_base64: toBase64(data),
668
+ }) as Promise<StoredFile>;
669
+ },
670
+ async delete(fileId) {
671
+ return rpc(callId, {
672
+ type: "files_op",
673
+ op: "delete",
674
+ file_id: fileId,
675
+ }) as Promise<{ deleted: boolean }>;
676
+ },
677
+ };
678
+ }
679
+
680
+ /** Largest `ctx.files.store` payload. Mirrors the host's
681
+ * FILES_STORE_MAX_BYTES; `PYLON_MAX_UPLOAD_BYTES` can lower the host's. */
682
+ const FILES_STORE_MAX_BYTES = 25 * 1024 * 1024;
683
+
684
+ /** Base64 of bytes, or of a string's UTF-8 encoding. */
685
+ function toBase64(data: Uint8Array | ArrayBuffer | string): string {
686
+ const bytes =
687
+ typeof data === "string"
688
+ ? new TextEncoder().encode(data)
689
+ : data instanceof ArrayBuffer
690
+ ? new Uint8Array(data)
691
+ : data;
692
+ return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64");
693
+ }
694
+
643
695
  export function buildSsrFnCaller(
644
696
  callId: string,
645
697
  ): (name: string, args?: Record<string, unknown>) => Promise<unknown> {
@@ -1340,7 +1392,7 @@ function buildActionCtx(
1340
1392
  (err as any).code = code;
1341
1393
  return err;
1342
1394
  },
1343
- files: buildFiles(callId),
1395
+ files: buildActionFiles(callId),
1344
1396
  shards: buildShards(callId),
1345
1397
  // Actions have no ctx.db; read membership via the built-in internal query.
1346
1398
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
@@ -1532,7 +1584,10 @@ async function handleCall(msg: CallMessage): Promise<void> {
1532
1584
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
1533
1585
  reader.query(entity, { ...filter, $limit: 1 }),
1534
1586
  ),
1535
- files: buildFiles(msg.call_id),
1587
+ // Typed as read-only `Files`; `store`/`delete` still exist at runtime
1588
+ // so a call from a query or mutation gets the host's
1589
+ // FILES_WRITE_NOT_ALLOWED instead of a TypeError.
1590
+ files: buildActionFiles(msg.call_id),
1536
1591
  shards: buildShards(msg.call_id),
1537
1592
  };
1538
1593
  break;
@@ -1558,7 +1613,10 @@ async function handleCall(msg: CallMessage): Promise<void> {
1558
1613
  requireMember: makeRequireMember(auth.userId, (entity, filter) =>
1559
1614
  writer.query(entity, { ...filter, $limit: 1 }),
1560
1615
  ),
1561
- files: buildFiles(msg.call_id),
1616
+ // Typed as read-only `Files`; `store`/`delete` still exist at runtime
1617
+ // so a call from a query or mutation gets the host's
1618
+ // FILES_WRITE_NOT_ALLOWED instead of a TypeError.
1619
+ files: buildActionFiles(msg.call_id),
1562
1620
  shards: buildShards(msg.call_id),
1563
1621
  };
1564
1622
  break;
package/src/types.ts CHANGED
@@ -992,6 +992,48 @@ export interface Files {
992
992
  signedUrl(fileId: string, opts?: { ttlSecs?: number }): Promise<string>;
993
993
  }
994
994
 
995
+ /** Options for {@link ActionFiles.store}. */
996
+ export interface StoreFileOptions {
997
+ /** File name, 1-255 characters, no path separators. */
998
+ name: string;
999
+ /** Defaults to `application/octet-stream`. */
1000
+ contentType?: string;
1001
+ /** Readable by anyone, signed in or not. Defaults to false. */
1002
+ public?: boolean;
1003
+ }
1004
+
1005
+ /** A file written by {@link ActionFiles.store}. */
1006
+ export interface StoredFile {
1007
+ id: string;
1008
+ /** Where the file is served: `/api/files/<id>` or the storage CDN URL. */
1009
+ url: string;
1010
+ size: number;
1011
+ }
1012
+
1013
+ /**
1014
+ * `ctx.files` in an action: the read-side {@link Files} plus writes to the
1015
+ * app's file storage (the backend `/api/files` uses). Writes run only in
1016
+ * actions: a stored or deleted file cannot roll back with a query or
1017
+ * mutation.
1018
+ *
1019
+ * A stored file is owned by the calling user and tenant, as if that user
1020
+ * had uploaded it. With no user (a scheduled job, a public webhook) the
1021
+ * file is system-owned: readable through {@link Files.signedUrl}, by an
1022
+ * unscoped admin, or by anyone when `public` is set.
1023
+ */
1024
+ export interface ActionFiles extends Files {
1025
+ /**
1026
+ * Store bytes as a new file. At most 25 MiB, or `PYLON_MAX_UPLOAD_BYTES`
1027
+ * when that is lower.
1028
+ */
1029
+ store(
1030
+ data: Uint8Array | ArrayBuffer | string,
1031
+ opts: StoreFileOptions,
1032
+ ): Promise<StoredFile>;
1033
+ /** Delete a stored file. `deleted` is false when it did not exist. */
1034
+ delete(fileId: string): Promise<{ deleted: boolean }>;
1035
+ }
1036
+
995
1037
  /**
996
1038
  * Shard tickets: short-lived, signed permission to join one realtime shard,
997
1039
  * with claims the shard's authorization hooks read without a database
@@ -1436,7 +1478,7 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
1436
1478
  /** Environment variables / secrets. */
1437
1479
  env: Record<string, string>;
1438
1480
  /** Signed file-download URLs — see {@link Files}. */
1439
- files: Files;
1481
+ files: ActionFiles;
1440
1482
  /** Shard tickets, reads, and start/stop — see {@link Shards}. */
1441
1483
  shards: Shards;
1442
1484
  /** Run a registered query within its own read transaction. */