@zapier/zapier-sdk 0.41.1 → 0.42.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @zapier/zapier-sdk
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 55e8330: Support maxTime for SDK fetch & CLI curl - Relay extended timeout
8
+
9
+ ## 0.41.2
10
+
11
+ ### Patch Changes
12
+
13
+ - 3cf1a98: Fix connection resolution for no-auth apps
14
+
3
15
  ## 0.41.1
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -971,6 +971,7 @@ Make authenticated HTTP requests to any API through Zapier. Pass a connectionId
971
971
  | ↳ `body` | `string, custom, record` | ❌ | — | — | Request body — plain objects and JSON strings are auto-detected and Content-Type is set accordingly |
972
972
  | ↳ `connection` | `string, number` | ❌ | — | — | Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly. |
973
973
  | ↳ `callbackUrl` | `string` | ❌ | — | — | URL to send async response to (makes request async) |
974
+ | ↳ `maxTime` | `number` | ❌ | — | — | Maximum seconds to wait for a response. Honored on a best-effort basis; the server may silently enforce a lower ceiling. |
974
975
 
975
976
  **Returns:** `Promise<Response>`
976
977
 
package/dist/index.cjs CHANGED
@@ -544,6 +544,20 @@ var appsPlugin = ({ sdk }) => {
544
544
  var FetchUrlSchema = zod.z.union([zod.z.string(), zod.z.instanceof(URL)]).describe(
545
545
  "The full URL of the API endpoint to call (proxied through Zapier's Relay service)"
546
546
  );
547
+ var FetchInitZapierFieldsSchema = zod.z.object({
548
+ connectionId: ConnectionIdPropertySchema.optional().meta({
549
+ deprecated: true
550
+ }),
551
+ connection: ConnectionPropertySchema.optional(),
552
+ /** @deprecated Use connection instead */
553
+ authenticationId: AuthenticationIdPropertySchema.optional().meta({
554
+ deprecated: true
555
+ }),
556
+ callbackUrl: zod.z.string().optional().describe("URL to send async response to (makes request async)"),
557
+ maxTime: zod.z.number().int().positive().optional().describe(
558
+ "Maximum seconds to wait for a response. Honored on a best-effort basis; the server may silently enforce a lower ceiling."
559
+ )
560
+ });
547
561
  var FetchInitSchema = zod.z.object({
548
562
  method: zod.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]).optional().describe("HTTP method for the request (defaults to GET)"),
549
563
  headers: zod.z.record(zod.z.string(), zod.z.string()).optional().describe("HTTP headers to include in the request"),
@@ -554,17 +568,8 @@ var FetchInitSchema = zod.z.object({
554
568
  zod.z.record(zod.z.string(), zod.z.unknown())
555
569
  ]).optional().describe(
556
570
  "Request body \u2014 plain objects and JSON strings are auto-detected and Content-Type is set accordingly"
557
- ),
558
- connectionId: ConnectionIdPropertySchema.optional().meta({
559
- deprecated: true
560
- }),
561
- connection: ConnectionPropertySchema.optional(),
562
- /** @deprecated Use connection instead */
563
- authenticationId: AuthenticationIdPropertySchema.optional().meta({
564
- deprecated: true
565
- }),
566
- callbackUrl: zod.z.string().optional().describe("URL to send async response to (makes request async)")
567
- }).optional().describe(
571
+ )
572
+ }).extend(FetchInitZapierFieldsSchema.shape).optional().describe(
568
573
  "Request options including method, headers, body, and authentication"
569
574
  ).meta({
570
575
  aliases: { connectionId: "connection", authenticationId: "connection" }
@@ -717,6 +722,82 @@ function isResolvedAppLocator(appLocator) {
717
722
  return !!appLocator.implementationName;
718
723
  }
719
724
 
725
+ // src/utils/abort-utils.ts
726
+ function getAbortSignalApi() {
727
+ const api = AbortSignal;
728
+ return { any: api.any };
729
+ }
730
+ function createTimeoutError(timeoutMs) {
731
+ if (typeof DOMException !== "undefined") {
732
+ return new DOMException(
733
+ `The operation timed out after ${timeoutMs}ms`,
734
+ "TimeoutError"
735
+ );
736
+ }
737
+ const error = new Error(`The operation timed out after ${timeoutMs}ms`);
738
+ error.name = "TimeoutError";
739
+ return error;
740
+ }
741
+ function createTimeoutAbortSignal({
742
+ timeoutMs
743
+ }) {
744
+ const controller = new AbortController();
745
+ let timer = setTimeout(() => {
746
+ timer = void 0;
747
+ controller.abort(createTimeoutError(timeoutMs));
748
+ }, timeoutMs);
749
+ const maybeUnref = timer.unref;
750
+ if (typeof maybeUnref === "function") {
751
+ maybeUnref.call(timer);
752
+ }
753
+ return {
754
+ signal: controller.signal,
755
+ dispose: () => {
756
+ if (timer !== void 0) {
757
+ clearTimeout(timer);
758
+ timer = void 0;
759
+ }
760
+ }
761
+ };
762
+ }
763
+ function combineAbortSignals({
764
+ handles,
765
+ abortSignalApi = getAbortSignalApi()
766
+ }) {
767
+ if (handles.length === 0) return void 0;
768
+ if (handles.length === 1) return handles[0];
769
+ const disposeInputs = () => {
770
+ for (const handle of handles) handle.dispose();
771
+ };
772
+ if (abortSignalApi.any) {
773
+ return {
774
+ signal: abortSignalApi.any(handles.map((h) => h.signal)),
775
+ dispose: disposeInputs
776
+ };
777
+ }
778
+ const controller = new AbortController();
779
+ const listeners = [];
780
+ for (const { signal: source } of handles) {
781
+ if (source.aborted) {
782
+ controller.abort(source.reason);
783
+ return { signal: controller.signal, dispose: disposeInputs };
784
+ }
785
+ const handler = () => controller.abort(source.reason);
786
+ source.addEventListener("abort", handler, { once: true });
787
+ listeners.push({ source, handler });
788
+ }
789
+ return {
790
+ signal: controller.signal,
791
+ dispose: () => {
792
+ for (const { source, handler } of listeners) {
793
+ source.removeEventListener("abort", handler);
794
+ }
795
+ listeners.length = 0;
796
+ disposeInputs();
797
+ }
798
+ };
799
+ }
800
+
720
801
  // src/utils/type-guard-utils.ts
721
802
  function isPlainObject(value) {
722
803
  if (typeof value !== "object" || value === null) return false;
@@ -752,6 +833,34 @@ function getMethodMetadata() {
752
833
  return telemetryStore.getStore()?.methodMetadata;
753
834
  }
754
835
 
836
+ // src/utils/validation.ts
837
+ var validate = (schema, input) => {
838
+ const result = schema.safeParse(input);
839
+ if (!result.success) {
840
+ const errorMessages = result.error.issues.map((error) => {
841
+ const path = error.path.length > 0 ? error.path.join(".") : "input";
842
+ return `${path}: ${error.message}`;
843
+ });
844
+ const message = `Validation failed:
845
+ ${errorMessages.join("\n ")}`;
846
+ throw new ZapierValidationError(message, {
847
+ details: {
848
+ zodErrors: result.error.issues,
849
+ input
850
+ }
851
+ });
852
+ }
853
+ return result.data;
854
+ };
855
+ function createValidator(schema) {
856
+ return function validateFn(input) {
857
+ return validate(schema, input);
858
+ };
859
+ }
860
+ var validateOptions = (schema, options) => {
861
+ return validate(schema, options);
862
+ };
863
+
755
864
  // src/plugins/fetch/index.ts
756
865
  function transformUrlToRelayPath(url) {
757
866
  const targetUrl = new URL(url);
@@ -789,6 +898,35 @@ function inferContentType(body) {
789
898
  }
790
899
  return void 0;
791
900
  }
901
+ function rewrapIfMaxTimeTimeout({
902
+ error,
903
+ abortSignal,
904
+ maxTimeSeconds
905
+ }) {
906
+ if (maxTimeSeconds === void 0 || !abortSignal?.aborted) return error;
907
+ const reason = abortSignal.reason;
908
+ if (!reason || reason.name !== "TimeoutError") return error;
909
+ return new ZapierTimeoutError(
910
+ `fetch timed out after ${maxTimeSeconds}s (maxTime)`,
911
+ { cause: error }
912
+ );
913
+ }
914
+ function buildAbortHandle({
915
+ maxTimeSeconds,
916
+ callerSignal
917
+ }) {
918
+ const handles = [];
919
+ if (callerSignal) handles.push({ signal: callerSignal, dispose: () => {
920
+ } });
921
+ if (maxTimeSeconds !== void 0) {
922
+ handles.push(
923
+ createTimeoutAbortSignal({ timeoutMs: maxTimeSeconds * 1e3 })
924
+ );
925
+ }
926
+ return combineAbortSignals({ handles });
927
+ }
928
+ var FetchInitSdkValidationSchema = zod.z.looseObject(FetchInitZapierFieldsSchema.shape).optional();
929
+ var validateFetchInit = createValidator(FetchInitSdkValidationSchema);
792
930
  var fetchPlugin = ({ context }) => {
793
931
  return {
794
932
  fetch: async function fetch2(url, init) {
@@ -797,11 +935,15 @@ var fetchPlugin = ({ context }) => {
797
935
  const startTime = Date.now();
798
936
  const isNested = isTelemetryNested();
799
937
  try {
938
+ if (init) {
939
+ validateFetchInit(init);
940
+ }
800
941
  const {
801
942
  connectionId,
802
943
  connection,
803
944
  authenticationId,
804
945
  callbackUrl,
946
+ maxTime,
805
947
  ...fetchInit
806
948
  } = init || {};
807
949
  const resolvedConnectionId = await resolveConnectionId({
@@ -832,32 +974,49 @@ var fetchPlugin = ({ context }) => {
832
974
  if (callbackUrl) {
833
975
  headers["X-Relay-Callback-Url"] = callbackUrl;
834
976
  }
835
- const result = await api.fetch(relayPath, {
836
- method: fetchInit.method ?? "GET",
837
- body: fetchInit.body,
838
- headers,
839
- redirect: fetchInit.redirect,
840
- signal: fetchInit.signal,
841
- authRequired: true
977
+ if (maxTime !== void 0) {
978
+ headers["X-Zapier-Sdk-Max-Time"] = String(maxTime);
979
+ }
980
+ const abortHandle = buildAbortHandle({
981
+ maxTimeSeconds: maxTime,
982
+ callerSignal: fetchInit.signal
842
983
  });
843
- const relayError = result.headers.get("x-relay-error");
844
- if (relayError) {
845
- throw new ZapierRelayError(relayError, {
846
- statusCode: result.status
984
+ try {
985
+ const result = await api.fetch(relayPath, {
986
+ method: fetchInit.method ?? "GET",
987
+ body: fetchInit.body,
988
+ headers,
989
+ redirect: fetchInit.redirect,
990
+ signal: abortHandle?.signal,
991
+ authRequired: true
847
992
  });
848
- }
849
- if (!isNested) {
850
- context.eventEmission.emitMethodCalled({
851
- method_name: "fetch",
852
- execution_duration_ms: Date.now() - startTime,
853
- success_flag: true,
854
- error_message: null,
855
- error_type: null,
856
- argument_count: init ? 2 : 1,
857
- is_paginated: false
993
+ const relayError = result.headers.get("x-relay-error");
994
+ if (relayError) {
995
+ throw new ZapierRelayError(relayError, {
996
+ statusCode: result.status
997
+ });
998
+ }
999
+ if (!isNested) {
1000
+ context.eventEmission.emitMethodCalled({
1001
+ method_name: "fetch",
1002
+ execution_duration_ms: Date.now() - startTime,
1003
+ success_flag: true,
1004
+ error_message: null,
1005
+ error_type: null,
1006
+ argument_count: init ? 2 : 1,
1007
+ is_paginated: false
1008
+ });
1009
+ }
1010
+ return result;
1011
+ } catch (error) {
1012
+ throw rewrapIfMaxTimeTimeout({
1013
+ error,
1014
+ abortSignal: abortHandle?.signal,
1015
+ maxTimeSeconds: maxTime
858
1016
  });
1017
+ } finally {
1018
+ abortHandle?.dispose();
859
1019
  }
860
- return result;
861
1020
  } catch (error) {
862
1021
  if (!isNested) {
863
1022
  context.eventEmission.emitMethodCalled({
@@ -1145,34 +1304,6 @@ function toIterable(source) {
1145
1304
  return { [Symbol.asyncIterator]: () => source[Symbol.asyncIterator]() };
1146
1305
  }
1147
1306
 
1148
- // src/utils/validation.ts
1149
- var validate = (schema, input) => {
1150
- const result = schema.safeParse(input);
1151
- if (!result.success) {
1152
- const errorMessages = result.error.issues.map((error) => {
1153
- const path = error.path.length > 0 ? error.path.join(".") : "input";
1154
- return `${path}: ${error.message}`;
1155
- });
1156
- const message = `Validation failed:
1157
- ${errorMessages.join("\n ")}`;
1158
- throw new ZapierValidationError(message, {
1159
- details: {
1160
- zodErrors: result.error.issues,
1161
- input
1162
- }
1163
- });
1164
- }
1165
- return result.data;
1166
- };
1167
- function createValidator(schema) {
1168
- return function validateFn(input) {
1169
- return validate(schema, input);
1170
- };
1171
- }
1172
- var validateOptions = (schema, options) => {
1173
- return validate(schema, options);
1174
- };
1175
-
1176
1307
  // src/utils/function-utils.ts
1177
1308
  function extractCursor(data) {
1178
1309
  if (!data?.next) {
@@ -1970,7 +2101,7 @@ var connectionIdResolver = {
1970
2101
  try {
1971
2102
  const app = await sdk.getApp({ app: params.app });
1972
2103
  if (!app.data.auth_type) {
1973
- return { resolvedValue: null };
2104
+ return { resolvedValue: void 0 };
1974
2105
  }
1975
2106
  } catch {
1976
2107
  }
package/dist/index.d.mts CHANGED
@@ -1597,15 +1597,18 @@ interface ConnectionsPluginProvides {
1597
1597
  }
1598
1598
  declare const connectionsPlugin: Plugin<{}, ManifestPluginProvides["context"], ConnectionsPluginProvides>;
1599
1599
 
1600
+ interface ZapierFetchInitOptions extends RequestInit {
1601
+ /** @deprecated Use `connection` instead. */
1602
+ connectionId?: string | number;
1603
+ connection?: string | number;
1604
+ /** @deprecated Use `connection` instead. */
1605
+ authenticationId?: string | number;
1606
+ callbackUrl?: string;
1607
+ /** Maximum seconds to wait for a response, subject to a server-side limit. */
1608
+ maxTime?: number;
1609
+ }
1600
1610
  interface FetchPluginProvides {
1601
- fetch: (url: string | URL, init?: RequestInit & {
1602
- /** @deprecated Use `connection` instead. */
1603
- connectionId?: string | number;
1604
- connection?: string | number;
1605
- /** @deprecated Use `connection` instead. */
1606
- authenticationId?: string | number;
1607
- callbackUrl?: string;
1608
- }) => Promise<Response>;
1611
+ fetch: (url: string | URL, init?: ZapierFetchInitOptions) => Promise<Response>;
1609
1612
  context: {
1610
1613
  meta: {
1611
1614
  fetch: {
@@ -1631,14 +1634,6 @@ declare const fetchPlugin: Plugin<{}, // no SDK dependencies
1631
1634
  api: ApiClient;
1632
1635
  } & ConnectionsPluginProvides["context"] & EventEmissionContext, // requires api, connections, and eventEmission in context
1633
1636
  FetchPluginProvides>;
1634
- type ZapierFetchInitOptions = RequestInit & {
1635
- /** @deprecated Use `connection` instead. */
1636
- connectionId?: string | number;
1637
- connection?: string | number;
1638
- /** @deprecated Use `connection` instead. */
1639
- authenticationId?: string | number;
1640
- callbackUrl?: string;
1641
- };
1642
1637
 
1643
1638
  declare const RunActionSchema: z.ZodObject<{
1644
1639
  app: z.ZodString & {
package/dist/index.mjs CHANGED
@@ -542,6 +542,20 @@ var appsPlugin = ({ sdk }) => {
542
542
  var FetchUrlSchema = z.union([z.string(), z.instanceof(URL)]).describe(
543
543
  "The full URL of the API endpoint to call (proxied through Zapier's Relay service)"
544
544
  );
545
+ var FetchInitZapierFieldsSchema = z.object({
546
+ connectionId: ConnectionIdPropertySchema.optional().meta({
547
+ deprecated: true
548
+ }),
549
+ connection: ConnectionPropertySchema.optional(),
550
+ /** @deprecated Use connection instead */
551
+ authenticationId: AuthenticationIdPropertySchema.optional().meta({
552
+ deprecated: true
553
+ }),
554
+ callbackUrl: z.string().optional().describe("URL to send async response to (makes request async)"),
555
+ maxTime: z.number().int().positive().optional().describe(
556
+ "Maximum seconds to wait for a response. Honored on a best-effort basis; the server may silently enforce a lower ceiling."
557
+ )
558
+ });
545
559
  var FetchInitSchema = z.object({
546
560
  method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]).optional().describe("HTTP method for the request (defaults to GET)"),
547
561
  headers: z.record(z.string(), z.string()).optional().describe("HTTP headers to include in the request"),
@@ -552,17 +566,8 @@ var FetchInitSchema = z.object({
552
566
  z.record(z.string(), z.unknown())
553
567
  ]).optional().describe(
554
568
  "Request body \u2014 plain objects and JSON strings are auto-detected and Content-Type is set accordingly"
555
- ),
556
- connectionId: ConnectionIdPropertySchema.optional().meta({
557
- deprecated: true
558
- }),
559
- connection: ConnectionPropertySchema.optional(),
560
- /** @deprecated Use connection instead */
561
- authenticationId: AuthenticationIdPropertySchema.optional().meta({
562
- deprecated: true
563
- }),
564
- callbackUrl: z.string().optional().describe("URL to send async response to (makes request async)")
565
- }).optional().describe(
569
+ )
570
+ }).extend(FetchInitZapierFieldsSchema.shape).optional().describe(
566
571
  "Request options including method, headers, body, and authentication"
567
572
  ).meta({
568
573
  aliases: { connectionId: "connection", authenticationId: "connection" }
@@ -715,6 +720,82 @@ function isResolvedAppLocator(appLocator) {
715
720
  return !!appLocator.implementationName;
716
721
  }
717
722
 
723
+ // src/utils/abort-utils.ts
724
+ function getAbortSignalApi() {
725
+ const api = AbortSignal;
726
+ return { any: api.any };
727
+ }
728
+ function createTimeoutError(timeoutMs) {
729
+ if (typeof DOMException !== "undefined") {
730
+ return new DOMException(
731
+ `The operation timed out after ${timeoutMs}ms`,
732
+ "TimeoutError"
733
+ );
734
+ }
735
+ const error = new Error(`The operation timed out after ${timeoutMs}ms`);
736
+ error.name = "TimeoutError";
737
+ return error;
738
+ }
739
+ function createTimeoutAbortSignal({
740
+ timeoutMs
741
+ }) {
742
+ const controller = new AbortController();
743
+ let timer = setTimeout(() => {
744
+ timer = void 0;
745
+ controller.abort(createTimeoutError(timeoutMs));
746
+ }, timeoutMs);
747
+ const maybeUnref = timer.unref;
748
+ if (typeof maybeUnref === "function") {
749
+ maybeUnref.call(timer);
750
+ }
751
+ return {
752
+ signal: controller.signal,
753
+ dispose: () => {
754
+ if (timer !== void 0) {
755
+ clearTimeout(timer);
756
+ timer = void 0;
757
+ }
758
+ }
759
+ };
760
+ }
761
+ function combineAbortSignals({
762
+ handles,
763
+ abortSignalApi = getAbortSignalApi()
764
+ }) {
765
+ if (handles.length === 0) return void 0;
766
+ if (handles.length === 1) return handles[0];
767
+ const disposeInputs = () => {
768
+ for (const handle of handles) handle.dispose();
769
+ };
770
+ if (abortSignalApi.any) {
771
+ return {
772
+ signal: abortSignalApi.any(handles.map((h) => h.signal)),
773
+ dispose: disposeInputs
774
+ };
775
+ }
776
+ const controller = new AbortController();
777
+ const listeners = [];
778
+ for (const { signal: source } of handles) {
779
+ if (source.aborted) {
780
+ controller.abort(source.reason);
781
+ return { signal: controller.signal, dispose: disposeInputs };
782
+ }
783
+ const handler = () => controller.abort(source.reason);
784
+ source.addEventListener("abort", handler, { once: true });
785
+ listeners.push({ source, handler });
786
+ }
787
+ return {
788
+ signal: controller.signal,
789
+ dispose: () => {
790
+ for (const { source, handler } of listeners) {
791
+ source.removeEventListener("abort", handler);
792
+ }
793
+ listeners.length = 0;
794
+ disposeInputs();
795
+ }
796
+ };
797
+ }
798
+
718
799
  // src/utils/type-guard-utils.ts
719
800
  function isPlainObject(value) {
720
801
  if (typeof value !== "object" || value === null) return false;
@@ -750,6 +831,34 @@ function getMethodMetadata() {
750
831
  return telemetryStore.getStore()?.methodMetadata;
751
832
  }
752
833
 
834
+ // src/utils/validation.ts
835
+ var validate = (schema, input) => {
836
+ const result = schema.safeParse(input);
837
+ if (!result.success) {
838
+ const errorMessages = result.error.issues.map((error) => {
839
+ const path = error.path.length > 0 ? error.path.join(".") : "input";
840
+ return `${path}: ${error.message}`;
841
+ });
842
+ const message = `Validation failed:
843
+ ${errorMessages.join("\n ")}`;
844
+ throw new ZapierValidationError(message, {
845
+ details: {
846
+ zodErrors: result.error.issues,
847
+ input
848
+ }
849
+ });
850
+ }
851
+ return result.data;
852
+ };
853
+ function createValidator(schema) {
854
+ return function validateFn(input) {
855
+ return validate(schema, input);
856
+ };
857
+ }
858
+ var validateOptions = (schema, options) => {
859
+ return validate(schema, options);
860
+ };
861
+
753
862
  // src/plugins/fetch/index.ts
754
863
  function transformUrlToRelayPath(url) {
755
864
  const targetUrl = new URL(url);
@@ -787,6 +896,35 @@ function inferContentType(body) {
787
896
  }
788
897
  return void 0;
789
898
  }
899
+ function rewrapIfMaxTimeTimeout({
900
+ error,
901
+ abortSignal,
902
+ maxTimeSeconds
903
+ }) {
904
+ if (maxTimeSeconds === void 0 || !abortSignal?.aborted) return error;
905
+ const reason = abortSignal.reason;
906
+ if (!reason || reason.name !== "TimeoutError") return error;
907
+ return new ZapierTimeoutError(
908
+ `fetch timed out after ${maxTimeSeconds}s (maxTime)`,
909
+ { cause: error }
910
+ );
911
+ }
912
+ function buildAbortHandle({
913
+ maxTimeSeconds,
914
+ callerSignal
915
+ }) {
916
+ const handles = [];
917
+ if (callerSignal) handles.push({ signal: callerSignal, dispose: () => {
918
+ } });
919
+ if (maxTimeSeconds !== void 0) {
920
+ handles.push(
921
+ createTimeoutAbortSignal({ timeoutMs: maxTimeSeconds * 1e3 })
922
+ );
923
+ }
924
+ return combineAbortSignals({ handles });
925
+ }
926
+ var FetchInitSdkValidationSchema = z.looseObject(FetchInitZapierFieldsSchema.shape).optional();
927
+ var validateFetchInit = createValidator(FetchInitSdkValidationSchema);
790
928
  var fetchPlugin = ({ context }) => {
791
929
  return {
792
930
  fetch: async function fetch2(url, init) {
@@ -795,11 +933,15 @@ var fetchPlugin = ({ context }) => {
795
933
  const startTime = Date.now();
796
934
  const isNested = isTelemetryNested();
797
935
  try {
936
+ if (init) {
937
+ validateFetchInit(init);
938
+ }
798
939
  const {
799
940
  connectionId,
800
941
  connection,
801
942
  authenticationId,
802
943
  callbackUrl,
944
+ maxTime,
803
945
  ...fetchInit
804
946
  } = init || {};
805
947
  const resolvedConnectionId = await resolveConnectionId({
@@ -830,32 +972,49 @@ var fetchPlugin = ({ context }) => {
830
972
  if (callbackUrl) {
831
973
  headers["X-Relay-Callback-Url"] = callbackUrl;
832
974
  }
833
- const result = await api.fetch(relayPath, {
834
- method: fetchInit.method ?? "GET",
835
- body: fetchInit.body,
836
- headers,
837
- redirect: fetchInit.redirect,
838
- signal: fetchInit.signal,
839
- authRequired: true
975
+ if (maxTime !== void 0) {
976
+ headers["X-Zapier-Sdk-Max-Time"] = String(maxTime);
977
+ }
978
+ const abortHandle = buildAbortHandle({
979
+ maxTimeSeconds: maxTime,
980
+ callerSignal: fetchInit.signal
840
981
  });
841
- const relayError = result.headers.get("x-relay-error");
842
- if (relayError) {
843
- throw new ZapierRelayError(relayError, {
844
- statusCode: result.status
982
+ try {
983
+ const result = await api.fetch(relayPath, {
984
+ method: fetchInit.method ?? "GET",
985
+ body: fetchInit.body,
986
+ headers,
987
+ redirect: fetchInit.redirect,
988
+ signal: abortHandle?.signal,
989
+ authRequired: true
845
990
  });
846
- }
847
- if (!isNested) {
848
- context.eventEmission.emitMethodCalled({
849
- method_name: "fetch",
850
- execution_duration_ms: Date.now() - startTime,
851
- success_flag: true,
852
- error_message: null,
853
- error_type: null,
854
- argument_count: init ? 2 : 1,
855
- is_paginated: false
991
+ const relayError = result.headers.get("x-relay-error");
992
+ if (relayError) {
993
+ throw new ZapierRelayError(relayError, {
994
+ statusCode: result.status
995
+ });
996
+ }
997
+ if (!isNested) {
998
+ context.eventEmission.emitMethodCalled({
999
+ method_name: "fetch",
1000
+ execution_duration_ms: Date.now() - startTime,
1001
+ success_flag: true,
1002
+ error_message: null,
1003
+ error_type: null,
1004
+ argument_count: init ? 2 : 1,
1005
+ is_paginated: false
1006
+ });
1007
+ }
1008
+ return result;
1009
+ } catch (error) {
1010
+ throw rewrapIfMaxTimeTimeout({
1011
+ error,
1012
+ abortSignal: abortHandle?.signal,
1013
+ maxTimeSeconds: maxTime
856
1014
  });
1015
+ } finally {
1016
+ abortHandle?.dispose();
857
1017
  }
858
- return result;
859
1018
  } catch (error) {
860
1019
  if (!isNested) {
861
1020
  context.eventEmission.emitMethodCalled({
@@ -1143,34 +1302,6 @@ function toIterable(source) {
1143
1302
  return { [Symbol.asyncIterator]: () => source[Symbol.asyncIterator]() };
1144
1303
  }
1145
1304
 
1146
- // src/utils/validation.ts
1147
- var validate = (schema, input) => {
1148
- const result = schema.safeParse(input);
1149
- if (!result.success) {
1150
- const errorMessages = result.error.issues.map((error) => {
1151
- const path = error.path.length > 0 ? error.path.join(".") : "input";
1152
- return `${path}: ${error.message}`;
1153
- });
1154
- const message = `Validation failed:
1155
- ${errorMessages.join("\n ")}`;
1156
- throw new ZapierValidationError(message, {
1157
- details: {
1158
- zodErrors: result.error.issues,
1159
- input
1160
- }
1161
- });
1162
- }
1163
- return result.data;
1164
- };
1165
- function createValidator(schema) {
1166
- return function validateFn(input) {
1167
- return validate(schema, input);
1168
- };
1169
- }
1170
- var validateOptions = (schema, options) => {
1171
- return validate(schema, options);
1172
- };
1173
-
1174
1305
  // src/utils/function-utils.ts
1175
1306
  function extractCursor(data) {
1176
1307
  if (!data?.next) {
@@ -1968,7 +2099,7 @@ var connectionIdResolver = {
1968
2099
  try {
1969
2100
  const app = await sdk.getApp({ app: params.app });
1970
2101
  if (!app.data.auth_type) {
1971
- return { resolvedValue: null };
2102
+ return { resolvedValue: void 0 };
1972
2103
  }
1973
2104
  } catch {
1974
2105
  }
@@ -1,17 +1,20 @@
1
1
  import type { Plugin } from "../../types/plugin";
2
2
  import type { ApiClient } from "../../api";
3
3
  import type { ConnectionsPluginProvides } from "../connections";
4
- import type { z } from "zod";
4
+ import { z } from "zod";
5
5
  import type { EventEmissionContext } from "../eventEmission";
6
+ export interface ZapierFetchInitOptions extends RequestInit {
7
+ /** @deprecated Use `connection` instead. */
8
+ connectionId?: string | number;
9
+ connection?: string | number;
10
+ /** @deprecated Use `connection` instead. */
11
+ authenticationId?: string | number;
12
+ callbackUrl?: string;
13
+ /** Maximum seconds to wait for a response, subject to a server-side limit. */
14
+ maxTime?: number;
15
+ }
6
16
  export interface FetchPluginProvides {
7
- fetch: (url: string | URL, init?: RequestInit & {
8
- /** @deprecated Use `connection` instead. */
9
- connectionId?: string | number;
10
- connection?: string | number;
11
- /** @deprecated Use `connection` instead. */
12
- authenticationId?: string | number;
13
- callbackUrl?: string;
14
- }) => Promise<Response>;
17
+ fetch: (url: string | URL, init?: ZapierFetchInitOptions) => Promise<Response>;
15
18
  context: {
16
19
  meta: {
17
20
  fetch: {
@@ -37,12 +40,4 @@ export declare const fetchPlugin: Plugin<{}, // no SDK dependencies
37
40
  api: ApiClient;
38
41
  } & ConnectionsPluginProvides["context"] & EventEmissionContext, // requires api, connections, and eventEmission in context
39
42
  FetchPluginProvides>;
40
- export type ZapierFetchInitOptions = RequestInit & {
41
- /** @deprecated Use `connection` instead. */
42
- connectionId?: string | number;
43
- connection?: string | number;
44
- /** @deprecated Use `connection` instead. */
45
- authenticationId?: string | number;
46
- callbackUrl?: string;
47
- };
48
43
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/fetch/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAEhE,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAoE7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,CACL,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,IAAI,CAAC,EAAE,WAAW,GAAG;QACnB,4CAA4C;QAC5C,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAC7B,4CAA4C;QAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QACnC,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,KACE,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvB,OAAO,EAAE;QACP,IAAI,EAAE;YACJ,KAAK,EAAE;gBACL,WAAW,EAAE,MAAM,CAAC;gBACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;gBACnB,UAAU,EAAE,MAAM,EAAE,CAAC;gBACrB,UAAU,EAAE,MAAM,CAAC;gBACnB,eAAe,EAAE,KAAK,CAAC;oBAAE,IAAI,EAAE,MAAM,CAAC;oBAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAA;iBAAE,CAAC,CAAC;aAC/D,CAAC;SACH,CAAC;KACH,CAAC;CACH;AAED;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAC9B,EAAE,EAAE,sBAAsB;AAC1B,AADI,sBAAsB;AAC1B;IAAE,GAAG,EAAE,SAAS,CAAA;CAAE,GAAG,yBAAyB,CAAC,SAAS,CAAC,GACvD,oBAAoB,EAAE,0DAA0D;AAClF,mBAAmB,CAiIpB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,WAAW,GAAG;IACjD,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/plugins/fetch/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAMhE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAgB7D,MAAM,WAAW,sBAAuB,SAAQ,WAAW;IACzD,4CAA4C;IAC5C,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7B,4CAA4C;IAC5C,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA4GD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,CACL,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,IAAI,CAAC,EAAE,sBAAsB,KAC1B,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvB,OAAO,EAAE;QACP,IAAI,EAAE;YACJ,KAAK,EAAE;gBACL,WAAW,EAAE,MAAM,CAAC;gBACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;gBACnB,UAAU,EAAE,MAAM,EAAE,CAAC;gBACrB,UAAU,EAAE,MAAM,CAAC;gBACnB,eAAe,EAAE,KAAK,CAAC;oBAAE,IAAI,EAAE,MAAM,CAAC;oBAAC,MAAM,EAAE,CAAC,CAAC,SAAS,CAAA;iBAAE,CAAC,CAAC;aAC/D,CAAC;SACH,CAAC;KACH,CAAC;CACH;AAQD;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,MAAM,CAC9B,EAAE,EAAE,sBAAsB;AAC1B,AADI,sBAAsB;AAC1B;IAAE,GAAG,EAAE,SAAS,CAAA;CAAE,GAAG,yBAAyB,CAAC,SAAS,CAAC,GACvD,oBAAoB,EAAE,0DAA0D;AAClF,mBAAmB,CAiJpB,CAAC"}
@@ -1,11 +1,14 @@
1
- import { ZapierRelayError } from "../../types/errors";
2
- import { FetchUrlSchema, FetchInitSchema } from "./schemas";
1
+ import { ZapierRelayError, ZapierTimeoutError } from "../../types/errors";
2
+ import { FetchUrlSchema, FetchInitSchema, FetchInitZapierFieldsSchema, } from "./schemas";
3
+ import { z } from "zod";
3
4
  import { resolveConnectionId } from "../../utils/domain-utils";
5
+ import { combineAbortSignals, createTimeoutAbortSignal, } from "../../utils/abort-utils";
4
6
  import { isPlainObject } from "../../utils/type-guard-utils";
5
7
  // fetch() doesn't use createFunction/createPaginatedFunction (its signature
6
8
  // doesn't fit the single-options-object pattern), so we wire up telemetry
7
9
  // context tracking manually.
8
10
  import { isTelemetryNested, runWithTelemetryContext, } from "../../utils/telemetry-context";
11
+ import { createValidator } from "../../utils/validation";
9
12
  /**
10
13
  * Transforms full URLs into Relay format: /relay/{domain}/{path}
11
14
  */
@@ -53,6 +56,41 @@ function inferContentType(body) {
53
56
  }
54
57
  return undefined;
55
58
  }
59
+ /**
60
+ * If the request aborted because the client-side `maxTime` timer fired, rewrap the
61
+ * underlying error as a `ZapierTimeoutError` so callers (and telemetry) can
62
+ * distinguish it from a caller-triggered abort or an unrelated network
63
+ * cancellation. The original error is preserved as `cause`.
64
+ *
65
+ * Caller-triggered aborts are left unwrapped so the caller's own abort
66
+ * semantics (reason, name) propagate.
67
+ */
68
+ function rewrapIfMaxTimeTimeout({ error, abortSignal, maxTimeSeconds, }) {
69
+ if (maxTimeSeconds === undefined || !abortSignal?.aborted)
70
+ return error;
71
+ const reason = abortSignal.reason;
72
+ if (!reason || reason.name !== "TimeoutError")
73
+ return error;
74
+ return new ZapierTimeoutError(`fetch timed out after ${maxTimeSeconds}s (maxTime)`, { cause: error });
75
+ }
76
+ /**
77
+ * Builds an AbortSignal (and a dispose callback) from the optional maxTime
78
+ * timeout and an optional caller-provided signal. Callers must invoke
79
+ * `dispose()` when the request completes to release timers and listeners.
80
+ */
81
+ function buildAbortHandle({ maxTimeSeconds, callerSignal, }) {
82
+ const handles = [];
83
+ if (callerSignal)
84
+ handles.push({ signal: callerSignal, dispose: () => { } });
85
+ if (maxTimeSeconds !== undefined) {
86
+ handles.push(createTimeoutAbortSignal({ timeoutMs: maxTimeSeconds * 1000 }));
87
+ }
88
+ return combineAbortSignals({ handles });
89
+ }
90
+ const FetchInitSdkValidationSchema = z
91
+ .looseObject(FetchInitZapierFieldsSchema.shape)
92
+ .optional();
93
+ const validateFetchInit = createValidator(FetchInitSdkValidationSchema);
56
94
  /**
57
95
  * Fetch plugin — the primary way to make authenticated HTTP requests through Zapier's Relay service.
58
96
  * Mirrors the native fetch(url, init?) signature with additional Zapier-specific options.
@@ -65,7 +103,10 @@ export const fetchPlugin = ({ context }) => {
65
103
  const startTime = Date.now();
66
104
  const isNested = isTelemetryNested();
67
105
  try {
68
- const { connectionId, connection, authenticationId, callbackUrl, ...fetchInit } = init || {};
106
+ if (init) {
107
+ validateFetchInit(init);
108
+ }
109
+ const { connectionId, connection, authenticationId, callbackUrl, maxTime, ...fetchInit } = init || {};
69
110
  const resolvedConnectionId = await resolveConnectionId({
70
111
  connectionId,
71
112
  connection,
@@ -88,32 +129,51 @@ export const fetchPlugin = ({ context }) => {
88
129
  if (callbackUrl) {
89
130
  headers["X-Relay-Callback-Url"] = callbackUrl;
90
131
  }
91
- const result = await api.fetch(relayPath, {
92
- method: fetchInit.method ?? "GET",
93
- body: fetchInit.body,
94
- headers,
95
- redirect: fetchInit.redirect,
96
- signal: fetchInit.signal,
97
- authRequired: true,
132
+ if (maxTime !== undefined) {
133
+ headers["X-Zapier-Sdk-Max-Time"] = String(maxTime);
134
+ }
135
+ const abortHandle = buildAbortHandle({
136
+ maxTimeSeconds: maxTime,
137
+ callerSignal: fetchInit.signal,
98
138
  });
99
- const relayError = result.headers.get("x-relay-error");
100
- if (relayError) {
101
- throw new ZapierRelayError(relayError, {
102
- statusCode: result.status,
139
+ try {
140
+ const result = await api.fetch(relayPath, {
141
+ method: fetchInit.method ?? "GET",
142
+ body: fetchInit.body,
143
+ headers,
144
+ redirect: fetchInit.redirect,
145
+ signal: abortHandle?.signal,
146
+ authRequired: true,
103
147
  });
148
+ const relayError = result.headers.get("x-relay-error");
149
+ if (relayError) {
150
+ throw new ZapierRelayError(relayError, {
151
+ statusCode: result.status,
152
+ });
153
+ }
154
+ if (!isNested) {
155
+ context.eventEmission.emitMethodCalled({
156
+ method_name: "fetch",
157
+ execution_duration_ms: Date.now() - startTime,
158
+ success_flag: true,
159
+ error_message: null,
160
+ error_type: null,
161
+ argument_count: init ? 2 : 1,
162
+ is_paginated: false,
163
+ });
164
+ }
165
+ return result;
104
166
  }
105
- if (!isNested) {
106
- context.eventEmission.emitMethodCalled({
107
- method_name: "fetch",
108
- execution_duration_ms: Date.now() - startTime,
109
- success_flag: true,
110
- error_message: null,
111
- error_type: null,
112
- argument_count: init ? 2 : 1,
113
- is_paginated: false,
167
+ catch (error) {
168
+ throw rewrapIfMaxTimeTimeout({
169
+ error,
170
+ abortSignal: abortHandle?.signal,
171
+ maxTimeSeconds: maxTime,
114
172
  });
115
173
  }
116
- return result;
174
+ finally {
175
+ abortHandle?.dispose();
176
+ }
117
177
  }
118
178
  catch (error) {
119
179
  if (!isNested) {
@@ -1,5 +1,12 @@
1
1
  import { z } from "zod";
2
2
  export declare const FetchUrlSchema: z.ZodUnion<readonly [z.ZodString, z.ZodCustom<URL, URL>]>;
3
+ export declare const FetchInitZapierFieldsSchema: z.ZodObject<{
4
+ connectionId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
5
+ connection: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
6
+ authenticationId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
7
+ callbackUrl: z.ZodOptional<z.ZodString>;
8
+ maxTime: z.ZodOptional<z.ZodNumber>;
9
+ }, z.core.$strip>;
3
10
  export declare const FetchInitSchema: z.ZodOptional<z.ZodObject<{
4
11
  method: z.ZodOptional<z.ZodEnum<{
5
12
  POST: "POST";
@@ -16,5 +23,6 @@ export declare const FetchInitSchema: z.ZodOptional<z.ZodObject<{
16
23
  connection: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
17
24
  authenticationId: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
18
25
  callbackUrl: z.ZodOptional<z.ZodString>;
26
+ maxTime: z.ZodOptional<z.ZodNumber>;
19
27
  }, z.core.$strip>>;
20
28
  //# sourceMappingURL=schemas.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/plugins/fetch/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,cAAc,2DAIxB,CAAC;AAEJ,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;kBAwCxB,CAAC"}
1
+ {"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../../src/plugins/fetch/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,eAAO,MAAM,cAAc,2DAIxB,CAAC;AAEJ,eAAO,MAAM,2BAA2B;;;;;;iBAqBtC,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;kBA6BxB,CAAC"}
@@ -4,6 +4,26 @@ import { ConnectionIdPropertySchema, ConnectionPropertySchema, AuthenticationIdP
4
4
  export const FetchUrlSchema = z
5
5
  .union([z.string(), z.instanceof(URL)])
6
6
  .describe("The full URL of the API endpoint to call (proxied through Zapier's Relay service)");
7
+ export const FetchInitZapierFieldsSchema = z.object({
8
+ connectionId: ConnectionIdPropertySchema.optional().meta({
9
+ deprecated: true,
10
+ }),
11
+ connection: ConnectionPropertySchema.optional(),
12
+ /** @deprecated Use connection instead */
13
+ authenticationId: AuthenticationIdPropertySchema.optional().meta({
14
+ deprecated: true,
15
+ }),
16
+ callbackUrl: z
17
+ .string()
18
+ .optional()
19
+ .describe("URL to send async response to (makes request async)"),
20
+ maxTime: z
21
+ .number()
22
+ .int()
23
+ .positive()
24
+ .optional()
25
+ .describe("Maximum seconds to wait for a response. Honored on a best-effort basis; the server may silently enforce a lower ceiling."),
26
+ });
7
27
  export const FetchInitSchema = z
8
28
  .object({
9
29
  method: z
@@ -23,19 +43,8 @@ export const FetchInitSchema = z
23
43
  ])
24
44
  .optional()
25
45
  .describe("Request body — plain objects and JSON strings are auto-detected and Content-Type is set accordingly"),
26
- connectionId: ConnectionIdPropertySchema.optional().meta({
27
- deprecated: true,
28
- }),
29
- connection: ConnectionPropertySchema.optional(),
30
- /** @deprecated Use connection instead */
31
- authenticationId: AuthenticationIdPropertySchema.optional().meta({
32
- deprecated: true,
33
- }),
34
- callbackUrl: z
35
- .string()
36
- .optional()
37
- .describe("URL to send async response to (makes request async)"),
38
46
  })
47
+ .extend(FetchInitZapierFieldsSchema.shape)
39
48
  .optional()
40
49
  .describe("Request options including method, headers, body, and authentication")
41
50
  .meta({
@@ -30,7 +30,7 @@ export const connectionIdResolver = {
30
30
  try {
31
31
  const app = await sdk.getApp({ app: params.app });
32
32
  if (!app.data.auth_type) {
33
- return { resolvedValue: null };
33
+ return { resolvedValue: undefined };
34
34
  }
35
35
  }
36
36
  catch {
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Wraps the subset of `AbortSignal` static methods that `combineAbortSignals`
3
+ * needs. Serves two real uses:
4
+ *
5
+ * 1. Runtime feature detection. `AbortSignal.any()` isn't present on every
6
+ * platform we support (Node <19, older browsers, some sandboxed
7
+ * runtimes), so `combineAbortSignals` checks `any` at call time and
8
+ * falls back to manual listener wiring when it's missing.
9
+ * 2. Test injection. Tests pass a fake `abortSignalApi` to force a
10
+ * specific branch (native vs. fallback) regardless of what the current
11
+ * runtime actually supports.
12
+ *
13
+ * By default, `combineAbortSignals` reads the real globals via
14
+ * `getAbortSignalApi()`; callers rarely need to pass this explicitly.
15
+ */
16
+ export interface AbortSignalApi {
17
+ any?: (signals: AbortSignal[]) => AbortSignal;
18
+ }
19
+ export interface DisposableAbortSignal {
20
+ signal: AbortSignal;
21
+ /**
22
+ * Release the resources backing this signal (timers, event listeners).
23
+ * Safe to call multiple times.
24
+ */
25
+ dispose: () => void;
26
+ }
27
+ export declare function createTimeoutError(timeoutMs: number): Error;
28
+ /**
29
+ * Creates an AbortSignal that fires after `timeoutMs` using a cancellable
30
+ * `AbortController` + `setTimeout`. We intentionally don't delegate to the
31
+ * native `AbortSignal.timeout()` — its returned signal has no way to cancel
32
+ * the underlying timer, which would make `dispose()` a lie on that path and
33
+ * give us divergent abort-reason messages across runtimes.
34
+ *
35
+ * On Node-like runtimes we `unref()` the timer so a pending timeout doesn't
36
+ * keep the process alive. In browsers `.unref` is absent and the guard is a
37
+ * harmless no-op.
38
+ *
39
+ * The signal's abort reason is a `DOMException` with `name: "TimeoutError"`
40
+ * so callers can distinguish timeout-driven aborts from caller-driven ones.
41
+ *
42
+ * Always call `dispose()` when the signal is no longer needed to clear the
43
+ * pending timer.
44
+ */
45
+ export declare function createTimeoutAbortSignal({ timeoutMs, }: {
46
+ timeoutMs: number;
47
+ }): DisposableAbortSignal;
48
+ /**
49
+ * Combines multiple disposable abort handles so the result aborts when *any*
50
+ * source does. Uses native `AbortSignal.any()` when available; otherwise wires
51
+ * up listeners manually.
52
+ *
53
+ * Returns `undefined` when the array is empty, or passes through a single
54
+ * handle without wrapping it.
55
+ *
56
+ * Always call `dispose()` on the result when it is no longer needed. This
57
+ * removes event listeners from source signals and calls `dispose()` on every
58
+ * input handle (releasing timers, etc).
59
+ */
60
+ export declare function combineAbortSignals({ handles, abortSignalApi, }: {
61
+ handles: DisposableAbortSignal[];
62
+ abortSignalApi?: AbortSignalApi;
63
+ }): DisposableAbortSignal | undefined;
64
+ //# sourceMappingURL=abort-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort-utils.d.ts","sourceRoot":"","sources":["../../src/utils/abort-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,CAAC;CAC/C;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,WAAW,CAAC;IACpB;;;OAGG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAOD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAW3D;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,SAAS,GACV,EAAE;IACD,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,qBAAqB,CAuBxB;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,cAAoC,GACrC,EAAE;IACD,OAAO,EAAE,qBAAqB,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GAAG,qBAAqB,GAAG,SAAS,CAwCpC"}
@@ -0,0 +1,107 @@
1
+ function getAbortSignalApi() {
2
+ const api = AbortSignal;
3
+ return { any: api.any };
4
+ }
5
+ export function createTimeoutError(timeoutMs) {
6
+ if (typeof DOMException !== "undefined") {
7
+ return new DOMException(`The operation timed out after ${timeoutMs}ms`, "TimeoutError");
8
+ }
9
+ const error = new Error(`The operation timed out after ${timeoutMs}ms`);
10
+ error.name = "TimeoutError";
11
+ return error;
12
+ }
13
+ // ---------------------------------------------------------------------------
14
+ // createTimeoutAbortSignal
15
+ // ---------------------------------------------------------------------------
16
+ /**
17
+ * Creates an AbortSignal that fires after `timeoutMs` using a cancellable
18
+ * `AbortController` + `setTimeout`. We intentionally don't delegate to the
19
+ * native `AbortSignal.timeout()` — its returned signal has no way to cancel
20
+ * the underlying timer, which would make `dispose()` a lie on that path and
21
+ * give us divergent abort-reason messages across runtimes.
22
+ *
23
+ * On Node-like runtimes we `unref()` the timer so a pending timeout doesn't
24
+ * keep the process alive. In browsers `.unref` is absent and the guard is a
25
+ * harmless no-op.
26
+ *
27
+ * The signal's abort reason is a `DOMException` with `name: "TimeoutError"`
28
+ * so callers can distinguish timeout-driven aborts from caller-driven ones.
29
+ *
30
+ * Always call `dispose()` when the signal is no longer needed to clear the
31
+ * pending timer.
32
+ */
33
+ export function createTimeoutAbortSignal({ timeoutMs, }) {
34
+ const controller = new AbortController();
35
+ let timer = setTimeout(() => {
36
+ timer = undefined;
37
+ controller.abort(createTimeoutError(timeoutMs));
38
+ }, timeoutMs);
39
+ // Node (and Bun/Deno): don't let a pending timeout block process exit.
40
+ // Browsers: `unref` is undefined on the numeric handle; this is a no-op.
41
+ const maybeUnref = timer.unref;
42
+ if (typeof maybeUnref === "function") {
43
+ maybeUnref.call(timer);
44
+ }
45
+ return {
46
+ signal: controller.signal,
47
+ dispose: () => {
48
+ if (timer !== undefined) {
49
+ clearTimeout(timer);
50
+ timer = undefined;
51
+ }
52
+ },
53
+ };
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // combineAbortSignals
57
+ // ---------------------------------------------------------------------------
58
+ /**
59
+ * Combines multiple disposable abort handles so the result aborts when *any*
60
+ * source does. Uses native `AbortSignal.any()` when available; otherwise wires
61
+ * up listeners manually.
62
+ *
63
+ * Returns `undefined` when the array is empty, or passes through a single
64
+ * handle without wrapping it.
65
+ *
66
+ * Always call `dispose()` on the result when it is no longer needed. This
67
+ * removes event listeners from source signals and calls `dispose()` on every
68
+ * input handle (releasing timers, etc).
69
+ */
70
+ export function combineAbortSignals({ handles, abortSignalApi = getAbortSignalApi(), }) {
71
+ if (handles.length === 0)
72
+ return undefined;
73
+ if (handles.length === 1)
74
+ return handles[0];
75
+ const disposeInputs = () => {
76
+ for (const handle of handles)
77
+ handle.dispose();
78
+ };
79
+ if (abortSignalApi.any) {
80
+ return {
81
+ signal: abortSignalApi.any(handles.map((h) => h.signal)),
82
+ dispose: disposeInputs,
83
+ };
84
+ }
85
+ // Fallback: wire up listeners manually.
86
+ const controller = new AbortController();
87
+ const listeners = [];
88
+ for (const { signal: source } of handles) {
89
+ if (source.aborted) {
90
+ controller.abort(source.reason);
91
+ return { signal: controller.signal, dispose: disposeInputs };
92
+ }
93
+ const handler = () => controller.abort(source.reason);
94
+ source.addEventListener("abort", handler, { once: true });
95
+ listeners.push({ source, handler });
96
+ }
97
+ return {
98
+ signal: controller.signal,
99
+ dispose: () => {
100
+ for (const { source, handler } of listeners) {
101
+ source.removeEventListener("abort", handler);
102
+ }
103
+ listeners.length = 0;
104
+ disposeInputs();
105
+ },
106
+ };
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapier/zapier-sdk",
3
- "version": "0.41.1",
3
+ "version": "0.42.0",
4
4
  "description": "Complete Zapier SDK - combines all Zapier SDK packages",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",