@pylonsync/functions 0.5.5 → 0.5.7

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, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, } from "./types";
package/dist/types.d.ts CHANGED
@@ -775,6 +775,140 @@ 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
+ /** One domain-availability / pricing result from a search. */
823
+ export interface DomainAvailability {
824
+ domainName: string;
825
+ sld: string;
826
+ tld: string;
827
+ purchasable: boolean;
828
+ premium?: boolean;
829
+ /** First-term price (USD), when purchasable. */
830
+ purchasePrice?: number;
831
+ renewalPrice?: number;
832
+ purchaseType?: string;
833
+ /** Why it isn't purchasable, when unavailable. */
834
+ reason?: string | null;
835
+ }
836
+ /** A registrant/admin/tech/billing contact for a domain registration. */
837
+ export interface DomainContact {
838
+ firstName: string;
839
+ lastName: string;
840
+ companyName?: string;
841
+ address1: string;
842
+ address2?: string;
843
+ city: string;
844
+ state: string;
845
+ zip: string;
846
+ country: string;
847
+ email: string;
848
+ phone: string;
849
+ fax?: string;
850
+ }
851
+ /** Options for {@link Domains.register}. `purchasePrice` is a price ceiling —
852
+ * registration is refused if the real price is higher, so a premium name can't
853
+ * surprise-charge. Contacts default to the platform account's when omitted. */
854
+ export interface RegisterDomainOptions {
855
+ years?: number;
856
+ autorenewEnabled?: boolean;
857
+ privacyEnabled?: boolean;
858
+ nameservers?: string[];
859
+ contacts?: {
860
+ registrant: DomainContact;
861
+ admin?: DomainContact;
862
+ tech?: DomainContact;
863
+ billing?: DomainContact;
864
+ };
865
+ purchasePrice?: number;
866
+ promoCode?: string;
867
+ }
868
+ /** Result of a domain registration. */
869
+ export interface RegisteredDomainResult {
870
+ domain: {
871
+ domainName: string;
872
+ createDate?: string;
873
+ expireDate?: string;
874
+ autorenewEnabled?: boolean;
875
+ locked?: boolean;
876
+ privacyEnabled?: boolean;
877
+ nameservers?: string[];
878
+ renewalPrice?: number;
879
+ };
880
+ order?: number;
881
+ totalPaid?: number;
882
+ }
883
+ export interface Domains {
884
+ /** Attach a custom domain for one of your end-customers. Returns the DNS the
885
+ * customer must set (the CNAME target + ownership/DCV TXT records). */
886
+ add(hostname: string): Promise<TenantDomainResult>;
887
+ /** Poll a tenant domain's verification + certificate status. */
888
+ status(hostname: string): Promise<TenantDomainResult>;
889
+ /** Detach a tenant domain (deletes the hostname + frees the slot). */
890
+ remove(hostname: string): Promise<{
891
+ hostname: string;
892
+ removed: boolean;
893
+ }>;
894
+ /** The hostnames currently ready (trusted) for this app — owner + tenant. */
895
+ list(): Promise<{
896
+ hosts: string[];
897
+ }>;
898
+ /** Search domain availability + pricing to buy. A `query` with a dot is an
899
+ * exact check ("acme.com"); a bare keyword ("acme") returns suggestions.
900
+ * Requires the registrar (name.com) to be configured on the control plane. */
901
+ search(query: string, opts?: {
902
+ suggest?: boolean;
903
+ tldFilter?: string[];
904
+ }): Promise<{
905
+ results: DomainAvailability[];
906
+ }>;
907
+ /** REGISTER (buy) a domain for your end-customer. Spends money on the
908
+ * platform's registrar account; requires a paid plan. Buying does not by
909
+ * itself serve the app — attach a host with `add()` afterward. */
910
+ register(domainName: string, opts?: RegisterDomainOptions): Promise<RegisteredDomainResult>;
911
+ }
778
912
  export interface ActionCtx<R extends AuthRequirement = "optional"> {
779
913
  auth: AuthInfo<R>;
780
914
  stream: Stream;
@@ -789,6 +923,9 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
789
923
  connections: Connections;
790
924
  /** Durable workflows: start / deliver events — see {@link Workflows}. */
791
925
  workflows: Workflows;
926
+ /** Attach / register custom domains for your app's OWN end-customers
927
+ * (platform domains) — see {@link Domains}. Cloud-only. */
928
+ domains: Domains;
792
929
  /** Environment variables / secrets. */
793
930
  env: Record<string, string>;
794
931
  /** 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.7",
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,13 @@ 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,
98
+ DomainAvailability,
99
+ DomainContact,
100
+ RegisterDomainOptions,
101
+ RegisteredDomainResult,
93
102
  } from "./types";
package/src/runtime.ts CHANGED
@@ -31,6 +31,11 @@ import type {
31
31
  Rooms,
32
32
  Workflows,
33
33
  Connections,
34
+ Domains,
35
+ TenantDomainResult,
36
+ DomainAvailability,
37
+ RegisterDomainOptions,
38
+ RegisteredDomainResult,
34
39
  QueryCtx,
35
40
  MutationCtx,
36
41
  ActionCtx,
@@ -1056,6 +1061,80 @@ function buildConnections(callId: string): Connections {
1056
1061
  };
1057
1062
  }
1058
1063
 
1064
+ /**
1065
+ * `ctx.domains` — platform (tenant) custom domains. Pure TS: proxies to the
1066
+ * Pylon Cloud control plane over HTTP, authed by the machine's own
1067
+ * `PYLON_DOMAINS_TOKEN` (a plain env var stamped at deploy — not a shared
1068
+ * credential, so no Rust-side transport hook like ctx.email needs). Fails LOUD
1069
+ * with `DOMAINS_NOT_CONFIGURED` when the cloud env is absent (self-host), rather
1070
+ * than silently no-opping — a call that looks like it attached a domain but
1071
+ * didn't would be a worse footgun.
1072
+ */
1073
+ function buildDomains(): Domains {
1074
+ const cloudUrl = (process.env.PYLON_CLOUD_URL || "").replace(/\/+$/, "");
1075
+ const token = process.env.PYLON_DOMAINS_TOKEN || "";
1076
+
1077
+ async function call<T>(
1078
+ endpoint: string,
1079
+ body: Record<string, unknown>,
1080
+ ): Promise<T> {
1081
+ if (!cloudUrl || !token) {
1082
+ const e = new Error(
1083
+ "custom domains require Pylon Cloud (PYLON_CLOUD_URL / PYLON_DOMAINS_TOKEN are not set on this machine)",
1084
+ );
1085
+ (e as any).code = "DOMAINS_NOT_CONFIGURED";
1086
+ throw e;
1087
+ }
1088
+ const res = await fetch(`${cloudUrl}/api/fn/${endpoint}`, {
1089
+ method: "POST",
1090
+ headers: {
1091
+ "Content-Type": "application/json",
1092
+ Authorization: `Bearer ${token}`,
1093
+ },
1094
+ body: JSON.stringify(body),
1095
+ });
1096
+ const text = await res.text();
1097
+ let json: any = {};
1098
+ try {
1099
+ json = text ? JSON.parse(text) : {};
1100
+ } catch {
1101
+ json = {};
1102
+ }
1103
+ // /api/fn returns the handler's raw value on 200, or
1104
+ // {error:{code,message}} on failure (crates/router json_error).
1105
+ if (!res.ok || json?.error) {
1106
+ const err = new Error(
1107
+ json?.error?.message || `platform-domains ${endpoint} failed (${res.status})`,
1108
+ );
1109
+ (err as any).code = json?.error?.code || "DOMAINS_REQUEST_FAILED";
1110
+ throw err;
1111
+ }
1112
+ return json as T;
1113
+ }
1114
+
1115
+ return {
1116
+ add: (hostname: string) =>
1117
+ call<TenantDomainResult>("provisionTenantDomain", { hostname }),
1118
+ status: (hostname: string) =>
1119
+ call<TenantDomainResult>("getTenantDomainStatus", { hostname }),
1120
+ remove: (hostname: string) =>
1121
+ call<{ hostname: string; removed: boolean }>("removeTenantDomain", {
1122
+ hostname,
1123
+ }),
1124
+ list: () => call<{ hosts: string[] }>("listProjectTrustedHosts", {}),
1125
+ search: (query: string, opts?: { suggest?: boolean; tldFilter?: string[] }) =>
1126
+ call<{ results: DomainAvailability[] }>("searchAvailableDomains", {
1127
+ query,
1128
+ ...(opts ?? {}),
1129
+ }),
1130
+ register: (domainName: string, opts?: RegisterDomainOptions) =>
1131
+ call<RegisteredDomainResult>("registerCustomerDomain", {
1132
+ domainName,
1133
+ ...(opts ?? {}),
1134
+ }),
1135
+ };
1136
+ }
1137
+
1059
1138
  function buildActionCtx(
1060
1139
  callId: string,
1061
1140
  auth: AuthInfo,
@@ -1089,6 +1168,7 @@ function buildActionCtx(
1089
1168
  rooms,
1090
1169
  connections,
1091
1170
  workflows: buildWorkflows(callId),
1171
+ domains: buildDomains(),
1092
1172
  env: process.env as Record<string, string>,
1093
1173
  async runQuery(fnName, args) {
1094
1174
  return rpc(callId, {
package/src/types.ts CHANGED
@@ -873,6 +873,143 @@ 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
+ /** One domain-availability / pricing result from a search. */
923
+ export interface DomainAvailability {
924
+ domainName: string;
925
+ sld: string;
926
+ tld: string;
927
+ purchasable: boolean;
928
+ premium?: boolean;
929
+ /** First-term price (USD), when purchasable. */
930
+ purchasePrice?: number;
931
+ renewalPrice?: number;
932
+ purchaseType?: string;
933
+ /** Why it isn't purchasable, when unavailable. */
934
+ reason?: string | null;
935
+ }
936
+
937
+ /** A registrant/admin/tech/billing contact for a domain registration. */
938
+ export interface DomainContact {
939
+ firstName: string;
940
+ lastName: string;
941
+ companyName?: string;
942
+ address1: string;
943
+ address2?: string;
944
+ city: string;
945
+ state: string;
946
+ zip: string;
947
+ country: string;
948
+ email: string;
949
+ phone: string;
950
+ fax?: string;
951
+ }
952
+
953
+ /** Options for {@link Domains.register}. `purchasePrice` is a price ceiling —
954
+ * registration is refused if the real price is higher, so a premium name can't
955
+ * surprise-charge. Contacts default to the platform account's when omitted. */
956
+ export interface RegisterDomainOptions {
957
+ years?: number;
958
+ autorenewEnabled?: boolean;
959
+ privacyEnabled?: boolean;
960
+ nameservers?: string[];
961
+ contacts?: {
962
+ registrant: DomainContact;
963
+ admin?: DomainContact;
964
+ tech?: DomainContact;
965
+ billing?: DomainContact;
966
+ };
967
+ purchasePrice?: number;
968
+ promoCode?: string;
969
+ }
970
+
971
+ /** Result of a domain registration. */
972
+ export interface RegisteredDomainResult {
973
+ domain: {
974
+ domainName: string;
975
+ createDate?: string;
976
+ expireDate?: string;
977
+ autorenewEnabled?: boolean;
978
+ locked?: boolean;
979
+ privacyEnabled?: boolean;
980
+ nameservers?: string[];
981
+ renewalPrice?: number;
982
+ };
983
+ order?: number;
984
+ totalPaid?: number;
985
+ }
986
+
987
+ export interface Domains {
988
+ /** Attach a custom domain for one of your end-customers. Returns the DNS the
989
+ * customer must set (the CNAME target + ownership/DCV TXT records). */
990
+ add(hostname: string): Promise<TenantDomainResult>;
991
+ /** Poll a tenant domain's verification + certificate status. */
992
+ status(hostname: string): Promise<TenantDomainResult>;
993
+ /** Detach a tenant domain (deletes the hostname + frees the slot). */
994
+ remove(hostname: string): Promise<{ hostname: string; removed: boolean }>;
995
+ /** The hostnames currently ready (trusted) for this app — owner + tenant. */
996
+ list(): Promise<{ hosts: string[] }>;
997
+ /** Search domain availability + pricing to buy. A `query` with a dot is an
998
+ * exact check ("acme.com"); a bare keyword ("acme") returns suggestions.
999
+ * Requires the registrar (name.com) to be configured on the control plane. */
1000
+ search(
1001
+ query: string,
1002
+ opts?: { suggest?: boolean; tldFilter?: string[] },
1003
+ ): Promise<{ results: DomainAvailability[] }>;
1004
+ /** REGISTER (buy) a domain for your end-customer. Spends money on the
1005
+ * platform's registrar account; requires a paid plan. Buying does not by
1006
+ * itself serve the app — attach a host with `add()` afterward. */
1007
+ register(
1008
+ domainName: string,
1009
+ opts?: RegisterDomainOptions,
1010
+ ): Promise<RegisteredDomainResult>;
1011
+ }
1012
+
876
1013
  export interface ActionCtx<R extends AuthRequirement = "optional"> {
877
1014
  auth: AuthInfo<R>;
878
1015
  stream: Stream;
@@ -887,6 +1024,9 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
887
1024
  connections: Connections;
888
1025
  /** Durable workflows: start / deliver events — see {@link Workflows}. */
889
1026
  workflows: Workflows;
1027
+ /** Attach / register custom domains for your app's OWN end-customers
1028
+ * (platform domains) — see {@link Domains}. Cloud-only. */
1029
+ domains: Domains;
890
1030
  /** Environment variables / secrets. */
891
1031
  env: Record<string, string>;
892
1032
  /** Signed file-download URLs — see {@link Files}. */