@pylonsync/functions 0.5.5 → 0.5.6

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
@@ -26,4 +26,4 @@ export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunne
26
26
  export { resetDb, installTestIsolation } from "./testing";
27
27
  export { slugifyName, availableSlug } from "./slugify";
28
28
  export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
29
- export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, MemberRow, Workflows, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, } from "./types";
29
+ export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, MemberRow, Workflows, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainDns, } from "./types";
package/dist/types.d.ts CHANGED
@@ -775,6 +775,66 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
775
775
  signal?: AbortSignal;
776
776
  }
777
777
  /** Context for action handlers (external I/O, non-transactional). */
778
+ /** One DNS record a tenant must set (name + value), as returned by the
779
+ * registrar/CDN. Used for the CNAME target's DCV + ownership TXT records. */
780
+ export interface TenantDomainDns {
781
+ name: string;
782
+ value: string;
783
+ }
784
+ /** The result of attaching / polling a tenant (platform) custom domain —
785
+ * everything your app shows its end-customer to complete DNS setup. */
786
+ export interface TenantDomainResult {
787
+ hostname: string;
788
+ /** "provisioning" until the cert is active, then "ready". */
789
+ status: string;
790
+ /** Whether the hostname + TLS certificate are both active. */
791
+ active?: boolean;
792
+ /** The single CNAME the customer points their hostname at. */
793
+ cnameTarget: string;
794
+ /** TXT record proving domain ownership before DNS cutover (or null). */
795
+ ownership: TenantDomainDns | null;
796
+ /** TXT records that issue the DV certificate. */
797
+ dcv: TenantDomainDns[];
798
+ /** Human-readable provisioning errors, empty when healthy. */
799
+ errors: string[];
800
+ }
801
+ /**
802
+ * Platform domains: attach / register / detach custom domains for your app's
803
+ * OWN end-customers, so each tenant can reach your app on their own hostname.
804
+ *
805
+ * Cloud-only — proxies to the Pylon Cloud control plane (which holds the
806
+ * Cloudflare/registrar credentials the app never sees). On a self-hosted Pylon
807
+ * every method throws a typed `DOMAINS_NOT_CONFIGURED` error.
808
+ *
809
+ * A newly attached hostname is trusted by the app's CORS / WebSocket / SSR
810
+ * gates automatically (the runtime refreshes the trusted-host set from the
811
+ * control plane) — no redeploy per tenant.
812
+ *
813
+ * @example
814
+ * ```ts
815
+ * // In an action, when your customer adds their domain:
816
+ * const d = await ctx.domains.add("app.customer.com");
817
+ * // Show them: CNAME app.customer.com -> d.cnameTarget, plus d.dcv TXT records.
818
+ * // Later, poll:
819
+ * const s = await ctx.domains.status("app.customer.com"); // s.active === true
820
+ * ```
821
+ */
822
+ export interface Domains {
823
+ /** Attach a custom domain for one of your end-customers. Returns the DNS the
824
+ * customer must set (the CNAME target + ownership/DCV TXT records). */
825
+ add(hostname: string): Promise<TenantDomainResult>;
826
+ /** Poll a tenant domain's verification + certificate status. */
827
+ status(hostname: string): Promise<TenantDomainResult>;
828
+ /** Detach a tenant domain (deletes the hostname + frees the slot). */
829
+ remove(hostname: string): Promise<{
830
+ hostname: string;
831
+ removed: boolean;
832
+ }>;
833
+ /** The hostnames currently ready (trusted) for this app — owner + tenant. */
834
+ list(): Promise<{
835
+ hosts: string[];
836
+ }>;
837
+ }
778
838
  export interface ActionCtx<R extends AuthRequirement = "optional"> {
779
839
  auth: AuthInfo<R>;
780
840
  stream: Stream;
@@ -789,6 +849,9 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
789
849
  connections: Connections;
790
850
  /** Durable workflows: start / deliver events — see {@link Workflows}. */
791
851
  workflows: Workflows;
852
+ /** Attach / register custom domains for your app's OWN end-customers
853
+ * (platform domains) — see {@link Domains}. Cloud-only. */
854
+ domains: Domains;
792
855
  /** Environment variables / secrets. */
793
856
  env: Record<string, string>;
794
857
  /** Signed file-download URLs — see {@link Files}. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
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
@@ -90,4 +90,9 @@ export type {
90
90
  LlmCompleteResponse,
91
91
  LlmStreamEvent,
92
92
  Rooms,
93
+ // Platform domains — app authors name these when handling ctx.domains
94
+ // results (the DNS the end-customer must set).
95
+ Domains,
96
+ TenantDomainResult,
97
+ TenantDomainDns,
93
98
  } from "./types";
package/src/runtime.ts CHANGED
@@ -31,6 +31,8 @@ import type {
31
31
  Rooms,
32
32
  Workflows,
33
33
  Connections,
34
+ Domains,
35
+ TenantDomainResult,
34
36
  QueryCtx,
35
37
  MutationCtx,
36
38
  ActionCtx,
@@ -1056,6 +1058,70 @@ function buildConnections(callId: string): Connections {
1056
1058
  };
1057
1059
  }
1058
1060
 
1061
+ /**
1062
+ * `ctx.domains` — platform (tenant) custom domains. Pure TS: proxies to the
1063
+ * Pylon Cloud control plane over HTTP, authed by the machine's own
1064
+ * `PYLON_DOMAINS_TOKEN` (a plain env var stamped at deploy — not a shared
1065
+ * credential, so no Rust-side transport hook like ctx.email needs). Fails LOUD
1066
+ * with `DOMAINS_NOT_CONFIGURED` when the cloud env is absent (self-host), rather
1067
+ * than silently no-opping — a call that looks like it attached a domain but
1068
+ * didn't would be a worse footgun.
1069
+ */
1070
+ function buildDomains(): Domains {
1071
+ const cloudUrl = (process.env.PYLON_CLOUD_URL || "").replace(/\/+$/, "");
1072
+ const token = process.env.PYLON_DOMAINS_TOKEN || "";
1073
+
1074
+ async function call<T>(
1075
+ endpoint: string,
1076
+ body: Record<string, unknown>,
1077
+ ): Promise<T> {
1078
+ if (!cloudUrl || !token) {
1079
+ const e = new Error(
1080
+ "custom domains require Pylon Cloud (PYLON_CLOUD_URL / PYLON_DOMAINS_TOKEN are not set on this machine)",
1081
+ );
1082
+ (e as any).code = "DOMAINS_NOT_CONFIGURED";
1083
+ throw e;
1084
+ }
1085
+ const res = await fetch(`${cloudUrl}/api/fn/${endpoint}`, {
1086
+ method: "POST",
1087
+ headers: {
1088
+ "Content-Type": "application/json",
1089
+ Authorization: `Bearer ${token}`,
1090
+ },
1091
+ body: JSON.stringify(body),
1092
+ });
1093
+ const text = await res.text();
1094
+ let json: any = {};
1095
+ try {
1096
+ json = text ? JSON.parse(text) : {};
1097
+ } catch {
1098
+ json = {};
1099
+ }
1100
+ // /api/fn returns the handler's raw value on 200, or
1101
+ // {error:{code,message}} on failure (crates/router json_error).
1102
+ if (!res.ok || json?.error) {
1103
+ const err = new Error(
1104
+ json?.error?.message || `platform-domains ${endpoint} failed (${res.status})`,
1105
+ );
1106
+ (err as any).code = json?.error?.code || "DOMAINS_REQUEST_FAILED";
1107
+ throw err;
1108
+ }
1109
+ return json as T;
1110
+ }
1111
+
1112
+ return {
1113
+ add: (hostname: string) =>
1114
+ call<TenantDomainResult>("provisionTenantDomain", { hostname }),
1115
+ status: (hostname: string) =>
1116
+ call<TenantDomainResult>("getTenantDomainStatus", { hostname }),
1117
+ remove: (hostname: string) =>
1118
+ call<{ hostname: string; removed: boolean }>("removeTenantDomain", {
1119
+ hostname,
1120
+ }),
1121
+ list: () => call<{ hosts: string[] }>("listProjectTrustedHosts", {}),
1122
+ };
1123
+ }
1124
+
1059
1125
  function buildActionCtx(
1060
1126
  callId: string,
1061
1127
  auth: AuthInfo,
@@ -1089,6 +1155,7 @@ function buildActionCtx(
1089
1155
  rooms,
1090
1156
  connections,
1091
1157
  workflows: buildWorkflows(callId),
1158
+ domains: buildDomains(),
1092
1159
  env: process.env as Record<string, string>,
1093
1160
  async runQuery(fnName, args) {
1094
1161
  return rpc(callId, {
package/src/types.ts CHANGED
@@ -873,6 +873,64 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
873
873
  }
874
874
 
875
875
  /** Context for action handlers (external I/O, non-transactional). */
876
+ /** One DNS record a tenant must set (name + value), as returned by the
877
+ * registrar/CDN. Used for the CNAME target's DCV + ownership TXT records. */
878
+ export interface TenantDomainDns {
879
+ name: string;
880
+ value: string;
881
+ }
882
+
883
+ /** The result of attaching / polling a tenant (platform) custom domain —
884
+ * everything your app shows its end-customer to complete DNS setup. */
885
+ export interface TenantDomainResult {
886
+ hostname: string;
887
+ /** "provisioning" until the cert is active, then "ready". */
888
+ status: string;
889
+ /** Whether the hostname + TLS certificate are both active. */
890
+ active?: boolean;
891
+ /** The single CNAME the customer points their hostname at. */
892
+ cnameTarget: string;
893
+ /** TXT record proving domain ownership before DNS cutover (or null). */
894
+ ownership: TenantDomainDns | null;
895
+ /** TXT records that issue the DV certificate. */
896
+ dcv: TenantDomainDns[];
897
+ /** Human-readable provisioning errors, empty when healthy. */
898
+ errors: string[];
899
+ }
900
+
901
+ /**
902
+ * Platform domains: attach / register / detach custom domains for your app's
903
+ * OWN end-customers, so each tenant can reach your app on their own hostname.
904
+ *
905
+ * Cloud-only — proxies to the Pylon Cloud control plane (which holds the
906
+ * Cloudflare/registrar credentials the app never sees). On a self-hosted Pylon
907
+ * every method throws a typed `DOMAINS_NOT_CONFIGURED` error.
908
+ *
909
+ * A newly attached hostname is trusted by the app's CORS / WebSocket / SSR
910
+ * gates automatically (the runtime refreshes the trusted-host set from the
911
+ * control plane) — no redeploy per tenant.
912
+ *
913
+ * @example
914
+ * ```ts
915
+ * // In an action, when your customer adds their domain:
916
+ * const d = await ctx.domains.add("app.customer.com");
917
+ * // Show them: CNAME app.customer.com -> d.cnameTarget, plus d.dcv TXT records.
918
+ * // Later, poll:
919
+ * const s = await ctx.domains.status("app.customer.com"); // s.active === true
920
+ * ```
921
+ */
922
+ export interface Domains {
923
+ /** Attach a custom domain for one of your end-customers. Returns the DNS the
924
+ * customer must set (the CNAME target + ownership/DCV TXT records). */
925
+ add(hostname: string): Promise<TenantDomainResult>;
926
+ /** Poll a tenant domain's verification + certificate status. */
927
+ status(hostname: string): Promise<TenantDomainResult>;
928
+ /** Detach a tenant domain (deletes the hostname + frees the slot). */
929
+ remove(hostname: string): Promise<{ hostname: string; removed: boolean }>;
930
+ /** The hostnames currently ready (trusted) for this app — owner + tenant. */
931
+ list(): Promise<{ hosts: string[] }>;
932
+ }
933
+
876
934
  export interface ActionCtx<R extends AuthRequirement = "optional"> {
877
935
  auth: AuthInfo<R>;
878
936
  stream: Stream;
@@ -887,6 +945,9 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
887
945
  connections: Connections;
888
946
  /** Durable workflows: start / deliver events — see {@link Workflows}. */
889
947
  workflows: Workflows;
948
+ /** Attach / register custom domains for your app's OWN end-customers
949
+ * (platform domains) — see {@link Domains}. Cloud-only. */
950
+ domains: Domains;
890
951
  /** Environment variables / secrets. */
891
952
  env: Record<string, string>;
892
953
  /** Signed file-download URLs — see {@link Files}. */