@zapier/zapier-sdk 0.42.1 → 0.44.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +75 -73
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +309 -35
  5. package/dist/api/types.d.ts +29 -0
  6. package/dist/api/types.d.ts.map +1 -1
  7. package/dist/constants.d.ts +25 -0
  8. package/dist/constants.d.ts.map +1 -1
  9. package/dist/constants.js +32 -0
  10. package/dist/index.cjs +441 -44
  11. package/dist/index.d.mts +82 -4
  12. package/dist/index.mjs +437 -45
  13. package/dist/plugins/api/index.d.ts.map +1 -1
  14. package/dist/plugins/api/index.js +5 -1
  15. package/dist/plugins/createClientCredentials/index.d.ts.map +1 -1
  16. package/dist/plugins/createClientCredentials/index.js +1 -0
  17. package/dist/plugins/createClientCredentials/schemas.d.ts +1 -0
  18. package/dist/plugins/createClientCredentials/schemas.d.ts.map +1 -1
  19. package/dist/plugins/createClientCredentials/schemas.js +6 -0
  20. package/dist/plugins/fetch/index.d.ts.map +1 -1
  21. package/dist/plugins/fetch/index.js +15 -1
  22. package/dist/plugins/getInputFieldsSchema/schemas.js +1 -1
  23. package/dist/plugins/listInputFieldChoices/schemas.js +1 -1
  24. package/dist/plugins/listInputFields/schemas.js +1 -1
  25. package/dist/plugins/manifest/schemas.d.ts +2 -2
  26. package/dist/plugins/runAction/index.d.ts.map +1 -1
  27. package/dist/plugins/runAction/index.js +12 -1
  28. package/dist/plugins/runAction/schemas.js +1 -1
  29. package/dist/types/connections.d.ts +2 -2
  30. package/dist/types/connections.d.ts.map +1 -1
  31. package/dist/types/connections.js +5 -3
  32. package/dist/types/errors.d.ts +37 -0
  33. package/dist/types/errors.d.ts.map +1 -1
  34. package/dist/types/errors.js +18 -0
  35. package/dist/types/properties.js +1 -1
  36. package/dist/types/sdk.d.ts +7 -0
  37. package/dist/types/sdk.d.ts.map +1 -1
  38. package/dist/types/sdk.js +20 -0
  39. package/dist/utils/open-approval.d.ts +2 -0
  40. package/dist/utils/open-approval.d.ts.map +1 -0
  41. package/dist/utils/open-approval.js +13 -0
  42. package/dist/utils/open-url.d.ts +3 -0
  43. package/dist/utils/open-url.d.ts.map +1 -0
  44. package/dist/utils/open-url.js +72 -0
  45. package/package.json +3 -1
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var zod = require('zod');
4
+ var policyContext = require('@zapier/policy-context');
4
5
  var apps = require('@zapier/zapier-sdk-core/v0/schemas/apps');
5
6
  var connections = require('@zapier/zapier-sdk-core/v0/schemas/connections');
6
7
  var clientCredentials = require('@zapier/zapier-sdk-core/v0/schemas/client-credentials');
@@ -58,6 +59,16 @@ function parseIntEnvVar(name) {
58
59
  }
59
60
  var ZAPIER_MAX_NETWORK_RETRIES = parseIntEnvVar("ZAPIER_MAX_NETWORK_RETRIES") ?? 3;
60
61
  var ZAPIER_MAX_NETWORK_RETRY_DELAY_MS = parseIntEnvVar("ZAPIER_MAX_NETWORK_RETRY_DELAY_MS") ?? 6e4;
62
+ function getZapierIsInteractive() {
63
+ return globalThis.process?.env?.ZAPIER_IS_INTERACTIVE === "true";
64
+ }
65
+ function getZapierApprovalMode() {
66
+ const value = globalThis.process?.env?.ZAPIER_APPROVAL_MODE;
67
+ if (value === "poll" || value === "fail") return value;
68
+ return void 0;
69
+ }
70
+ var DEFAULT_APPROVAL_TIMEOUT_MS = 10 * 60 * 1e3;
71
+ var DEFAULT_MAX_APPROVAL_RETRIES = 2;
61
72
 
62
73
  // src/types/properties.ts
63
74
  var AppKeyPropertySchema = withPositional(
@@ -90,7 +101,7 @@ var AuthenticationIdPropertySchema = ConnectionIdPropertySchema.meta({
90
101
  deprecated: true
91
102
  });
92
103
  var ConnectionPropertySchema = zod.z.union([zod.z.string(), zod.z.number().int().positive()]).describe(
93
- "Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly."
104
+ "Connection alias or connection ID (UUID or positive integer). Strings that match a key in the connections map are resolved against it; otherwise the value is used as a connection ID directly."
94
105
  );
95
106
  var InputsPropertySchema = zod.z.record(zod.z.string(), zod.z.unknown()).describe("Input parameters for the action");
96
107
  var LimitPropertySchema = zod.z.number().int().min(1).max(MAX_PAGE_LIMIT).default(50).describe("Maximum number of items to return");
@@ -226,6 +237,17 @@ var ZapierRateLimitError = class extends ZapierError {
226
237
  this.retries = options.retries ?? 0;
227
238
  }
228
239
  };
240
+ var ZapierApprovalError = class extends ZapierError {
241
+ constructor(message, options = {}) {
242
+ super(message, options);
243
+ this.name = "ZapierApprovalError";
244
+ this.code = "ZAPIER_APPROVAL_ERROR";
245
+ this.approvalId = options.approvalId;
246
+ this.approvalStatus = options.status;
247
+ this.approvalUrl = options.approvalUrl;
248
+ this.pollUrl = options.pollUrl;
249
+ }
250
+ };
229
251
  var ZapierRelayError = class extends ZapierError {
230
252
  constructor(message, options = {}) {
231
253
  super(message, options);
@@ -270,6 +292,10 @@ ${context.join(", ")}`;
270
292
  if (error instanceof ZapierBundleError && error.buildErrors && error.buildErrors.length > 0) {
271
293
  message += "\n\nBuild errors:\n" + error.buildErrors.map((err) => ` \u2022 ${err}`).join("\n");
272
294
  }
295
+ if (error instanceof ZapierApprovalError && error.approvalUrl) {
296
+ message += `
297
+ Approval URL: ${error.approvalUrl}`;
298
+ }
273
299
  if (error instanceof ZapierRateLimitError) {
274
300
  const { limit, remaining, retryAfterMs } = error.rateLimit;
275
301
  const parts = [];
@@ -977,18 +1003,27 @@ var fetchPlugin = (sdk) => {
977
1003
  if (maxTime !== void 0) {
978
1004
  headers["X-Zapier-Sdk-Max-Time"] = String(maxTime);
979
1005
  }
1006
+ const upstreamUrl = new URL(url).toString();
1007
+ const method = (fetchInit.method ?? "GET").toUpperCase();
980
1008
  const abortHandle = buildAbortHandle({
981
1009
  maxTimeSeconds: maxTime,
982
1010
  callerSignal: fetchInit.signal
983
1011
  });
984
1012
  try {
985
1013
  const result = await api.fetch(relayPath, {
986
- method: fetchInit.method ?? "GET",
1014
+ method,
987
1015
  body: fetchInit.body,
988
1016
  headers,
989
1017
  redirect: fetchInit.redirect,
990
1018
  signal: abortHandle?.signal,
991
- authRequired: true
1019
+ authRequired: true,
1020
+ approvalContext: () => policyContext.buildHttpRequestContext({
1021
+ method,
1022
+ url: upstreamUrl,
1023
+ headers,
1024
+ body: typeof fetchInit.body === "string" ? fetchInit.body : void 0,
1025
+ connection_id: resolvedConnectionId ? String(resolvedConnectionId) : void 0
1026
+ })
992
1027
  });
993
1028
  const relayError = result.headers.get("x-relay-error");
994
1029
  if (relayError) {
@@ -2978,7 +3013,7 @@ var listActionsPlugin = (sdk) => {
2978
3013
  var ListInputFieldsDescription = "Get the input fields required for a specific action";
2979
3014
  var ListInputFieldsBaseSchema = zod.z.object({
2980
3015
  connection: ConnectionPropertySchema.optional().describe(
2981
- "Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly. Mutually exclusive with connectionId."
3016
+ "Connection alias or connection ID (UUID or positive integer). Strings that match a key in the connections map are resolved against it; otherwise the value is used as a connection ID directly. Mutually exclusive with connectionId."
2982
3017
  ),
2983
3018
  connectionId: ConnectionIdPropertySchema.nullable().optional().describe(
2984
3019
  "Connection ID to use when listing input fields. Required if the action needs a connection to determine available fields."
@@ -3674,7 +3709,9 @@ var listClientCredentialsPlugin = (sdk) => {
3674
3709
  };
3675
3710
  };
3676
3711
  var CreateClientCredentialsSchema = clientCredentials.CreateClientCredentialsRequestSchema.omit({ allowed_scopes: true }).extend({
3677
- allowedScopes: zod.z.array(zod.z.enum(["credentials", "external"])).default(["external"]).describe("Scopes to allow for these credentials")
3712
+ allowedScopes: zod.z.array(zod.z.enum(["credentials", "external"])).default(["external"]).describe("Scopes to allow for these credentials"),
3713
+ // Temporarily hidden while we finalise work to make approvals/policies customer-facing.
3714
+ policy: zod.z.record(zod.z.string(), zod.z.unknown()).optional().describe("Policy document (JSON) to attach to the credentials").meta({ deprecated: true })
3678
3715
  }).describe("Create new client credentials for the authenticated user");
3679
3716
 
3680
3717
  // src/plugins/createClientCredentials/index.ts
@@ -3685,7 +3722,8 @@ var createClientCredentialsPlugin = (sdk) => {
3685
3722
  "/api/v0/client-credentials",
3686
3723
  {
3687
3724
  name: options.name,
3688
- allowed_scopes: options.allowedScopes
3725
+ allowed_scopes: options.allowedScopes,
3726
+ ...options.policy && { policy: options.policy }
3689
3727
  },
3690
3728
  {
3691
3729
  customErrorHandler: ({ status }) => {
@@ -4075,7 +4113,7 @@ var findUniqueConnectionPlugin = (sdk) => {
4075
4113
  var RunActionDescription = "Execute an action with the given inputs";
4076
4114
  var RunActionBaseSchema = zod.z.object({
4077
4115
  connection: ConnectionPropertySchema.optional().describe(
4078
- "Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly. Mutually exclusive with connectionId."
4116
+ "Connection alias or connection ID (UUID or positive integer). Strings that match a key in the connections map are resolved against it; otherwise the value is used as a connection ID directly. Mutually exclusive with connectionId."
4079
4117
  ),
4080
4118
  connectionId: ConnectionIdPropertySchema.nullable().optional().describe(
4081
4119
  "Connection ID to use when running the action. Required if the action needs a connection to authenticate and interact with the service."
@@ -4108,8 +4146,6 @@ var RunActionSchemaDeprecated = zod.z.object({
4108
4146
  actionKey: ActionKeyPropertySchema
4109
4147
  }).merge(RunActionBaseSchema);
4110
4148
  var RunActionInputSchema = zod.z.union([RunActionSchema, RunActionSchemaDeprecated]).describe(RunActionDescription);
4111
-
4112
- // src/plugins/runAction/index.ts
4113
4149
  async function executeAction(actionOptions) {
4114
4150
  const {
4115
4151
  api,
@@ -4140,7 +4176,18 @@ async function executeAction(actionOptions) {
4140
4176
  };
4141
4177
  const runData = await api.post(
4142
4178
  "/zapier/api/actions/v1/runs",
4143
- runRequest
4179
+ runRequest,
4180
+ {
4181
+ approvalContext: () => policyContext.buildActionRunContext({
4182
+ selected_api: selectedApi,
4183
+ action_type: actionType,
4184
+ action_key: actionKey,
4185
+ connection_id: connectionId != null ? String(connectionId) : void 0,
4186
+ // Cast: inputs is Record<string, unknown> at the SDK surface, but
4187
+ // buildActionRunContext coerces to JsonValue at runtime via zod.
4188
+ inputs: executionOptions.inputs ?? {}
4189
+ })
4190
+ }
4144
4191
  );
4145
4192
  const runId = runData.data.id;
4146
4193
  return await api.poll(`/zapier/api/actions/v1/runs/${runId}`, {
@@ -4445,8 +4492,12 @@ async function readFile(filePath) {
4445
4492
  }
4446
4493
  throw new Error(`File not found: ${filePath}`);
4447
4494
  }
4495
+ var POSITIVE_INTEGER_OR_UUID = /^([1-9][0-9]*|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
4448
4496
  var ConnectionEntrySchema = zod.z.object({
4449
- connectionId: zod.z.number().int().positive().describe("Zapier connection ID for the third-party service.")
4497
+ connectionId: zod.z.union([
4498
+ zod.z.string().regex(POSITIVE_INTEGER_OR_UUID),
4499
+ zod.z.number().int().positive()
4500
+ ]).describe("Zapier connection ID for the third-party service.")
4450
4501
  });
4451
4502
  var ConnectionsMapSchema = zod.z.record(zod.z.string(), ConnectionEntrySchema);
4452
4503
 
@@ -5689,7 +5740,89 @@ async function invalidateCredentialsToken(options) {
5689
5740
  }
5690
5741
  }
5691
5742
 
5692
- // src/api/client.ts
5743
+ // src/utils/open-url.ts
5744
+ var nodePrefix = "node:";
5745
+ async function loadChildProcess() {
5746
+ return import(`${nodePrefix}child_process`);
5747
+ }
5748
+ async function loadProcess() {
5749
+ return import(`${nodePrefix}process`);
5750
+ }
5751
+ var openUrl = async (url) => {
5752
+ if (typeof url !== "string") {
5753
+ throw new TypeError("Expected `url` to be a string");
5754
+ }
5755
+ let parsed;
5756
+ try {
5757
+ parsed = new URL(url);
5758
+ } catch {
5759
+ throw new Error(`Invalid URL: ${url}`);
5760
+ }
5761
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
5762
+ throw new Error(`Refusing to open non-http(s) URL: ${parsed.protocol}`);
5763
+ }
5764
+ const target = parsed.toString();
5765
+ if (typeof globalThis.window !== "undefined" && typeof globalThis.window.open === "function") {
5766
+ globalThis.window.open(target, "_blank", "noopener");
5767
+ return;
5768
+ }
5769
+ let spawn;
5770
+ let platform;
5771
+ try {
5772
+ const [cp, proc] = await Promise.all([loadChildProcess(), loadProcess()]);
5773
+ spawn = cp.spawn;
5774
+ platform = proc.platform;
5775
+ } catch {
5776
+ throw new Error(
5777
+ "openUrl: no supported runtime. window.open is unavailable and node:child_process could not be loaded."
5778
+ );
5779
+ }
5780
+ if (typeof spawn !== "function") {
5781
+ throw new Error(
5782
+ "openUrl: child_process.spawn is not available in this runtime"
5783
+ );
5784
+ }
5785
+ let command;
5786
+ let args;
5787
+ if (platform === "darwin") {
5788
+ command = "open";
5789
+ args = [target];
5790
+ } else if (platform === "win32") {
5791
+ command = "rundll32";
5792
+ args = ["url.dll,FileProtocolHandler", target];
5793
+ } else {
5794
+ throw new Error(`Unsupported platform: ${platform}`);
5795
+ }
5796
+ const child = spawn(command, args, {
5797
+ stdio: "ignore",
5798
+ detached: true,
5799
+ windowsHide: true
5800
+ });
5801
+ child.once("error", () => {
5802
+ });
5803
+ child.unref();
5804
+ };
5805
+ var open_url_default = openUrl;
5806
+
5807
+ // src/utils/open-approval.ts
5808
+ async function openApproval(url) {
5809
+ console.error(`Approval required. Opening browser: ${url}`);
5810
+ try {
5811
+ await open_url_default(url);
5812
+ } catch {
5813
+ }
5814
+ }
5815
+ var ApprovalStatusSchema = zod.z.enum(["pending_approval", "approved", "denied"]);
5816
+ var CreateApprovalResponseSchema = zod.z.object({
5817
+ status: ApprovalStatusSchema,
5818
+ approval_id: zod.z.string(),
5819
+ approval_url: zod.z.string().url(),
5820
+ poll_url: zod.z.string().url()
5821
+ });
5822
+ var PollApprovalResponseSchema = zod.z.object({
5823
+ status: ApprovalStatusSchema,
5824
+ approval_id: zod.z.string()
5825
+ });
5693
5826
  function parseRateLimitHeaders(response) {
5694
5827
  const info = {};
5695
5828
  const retryAfter = response.headers.get("retry-after");
@@ -5750,16 +5883,23 @@ var pathConfig = {
5750
5883
  var ZapierApiClient = class {
5751
5884
  constructor(options) {
5752
5885
  this.options = options;
5753
- this.fetch = async (path, init) => {
5754
- if (!path.startsWith("/")) {
5755
- throw new ZapierValidationError(
5756
- `fetch expects a path starting with '/', got: ${path}`
5757
- );
5758
- }
5886
+ /**
5887
+ * Perform a request against an already-resolved URL.
5888
+ *
5889
+ * Does auth, header merging, and 429 retry — all the cross-cutting
5890
+ * concerns that every Zapier-bound HTTP call needs. Callers that have a
5891
+ * path (e.g. `/relay/...`) should use `rawFetch` instead, which does
5892
+ * path → URL resolution and delegates here.
5893
+ *
5894
+ * Exposed as a separate helper so call sites with a server-supplied
5895
+ * absolute URL (e.g. an approval poll URL) can still share the same
5896
+ * auth/retry pipeline instead of reaching for `this.options.fetch`
5897
+ * directly and drifting.
5898
+ */
5899
+ this.rawFetchUrl = async (url, init, pathConfig2) => {
5759
5900
  if (init?.body && (isPlainObject(init.body) || Array.isArray(init.body))) {
5760
5901
  init.body = JSON.stringify(init.body);
5761
5902
  }
5762
- const { url, pathConfig: pathConfig2 } = this.buildUrl(path, init?.searchParams);
5763
5903
  const builtHeaders = await this.buildHeaders(
5764
5904
  init,
5765
5905
  pathConfig2
@@ -5778,30 +5918,114 @@ var ZapierApiClient = class {
5778
5918
  ...init,
5779
5919
  headers: mergedHeaders
5780
5920
  });
5781
- if (response.status === 429) {
5782
- const rateLimitInfo = parseRateLimitHeaders(response);
5783
- const delayMs = rateLimitInfo.retryAfterMs ?? calculateExponentialBackoffMs(retries + 1);
5784
- if (delayMs > this.maxNetworkRetryDelayMs || retries >= this.maxNetworkRetries) {
5785
- throw new ZapierRateLimitError("Rate limited", {
5786
- statusCode: 429,
5787
- rateLimit: rateLimitInfo,
5788
- retries
5789
- });
5790
- }
5791
- retries++;
5792
- this.emitEvent("api:rate_limit_retry", {
5793
- retry: retries,
5794
- maxNetworkRetries: this.maxNetworkRetries,
5795
- delayMs,
5796
- path,
5797
- method: init?.method ?? "GET",
5798
- rateLimit: rateLimitInfo
5921
+ if (response.status !== 429) {
5922
+ return response;
5923
+ }
5924
+ const rateLimitInfo = parseRateLimitHeaders(response);
5925
+ const delayMs = rateLimitInfo.retryAfterMs ?? calculateExponentialBackoffMs(retries + 1);
5926
+ if (delayMs > this.maxNetworkRetryDelayMs || retries >= this.maxNetworkRetries) {
5927
+ throw new ZapierRateLimitError("Rate limited", {
5928
+ statusCode: 429,
5929
+ rateLimit: rateLimitInfo,
5930
+ retries
5931
+ });
5932
+ }
5933
+ retries++;
5934
+ this.emitEvent("api:rate_limit_retry", {
5935
+ retry: retries,
5936
+ maxNetworkRetries: this.maxNetworkRetries,
5937
+ delayMs,
5938
+ path: url,
5939
+ method: init?.method ?? "GET",
5940
+ rateLimit: rateLimitInfo
5941
+ });
5942
+ await sleep(delayMs);
5943
+ }
5944
+ };
5945
+ /**
5946
+ * Perform a request with auth, header merging, and rate-limit (429) retries.
5947
+ * Does NOT handle 403 approval_required — that's routed by `fetch`.
5948
+ */
5949
+ this.rawFetch = async (path, init) => {
5950
+ if (!path.startsWith("/")) {
5951
+ throw new ZapierValidationError(
5952
+ `fetch expects a path starting with '/', got: ${path}`
5953
+ );
5954
+ }
5955
+ const { url, pathConfig: pathConfig2 } = this.buildUrl(path, init?.searchParams);
5956
+ return this.rawFetchUrl(url, init, pathConfig2);
5957
+ };
5958
+ /**
5959
+ * Approval-aware HTTP fetch.
5960
+ *
5961
+ * Wraps `rawFetch` with the backend's just-in-time approval protocol. The
5962
+ * backend signals approval state via a 403 response with an
5963
+ * `x-zapier-error-type` header:
5964
+ *
5965
+ * - `request_denied_by_policy` → a policy rule permanently blocks the
5966
+ * request; no human can approve it. Throw.
5967
+ * - `approval_required` → the request needs human approval; the
5968
+ * SDK creates an approval, opens the URL
5969
+ * (poll mode), waits for resolution, and
5970
+ * retries the original request.
5971
+ * - anything else → not our concern, pass through.
5972
+ *
5973
+ * The retry loop exists because a single user action can legitimately
5974
+ * require multiple sequential approvals (e.g. policies that approve one
5975
+ * step at a time). Each iteration is either a first attempt or a post-
5976
+ * approval retry; `maxApprovalRetries` bounds the loop as a runaway-loop
5977
+ * safeguard.
5978
+ *
5979
+ * Loop accounting: `attempt` counts rawFetch calls, not approval rounds.
5980
+ * With `maxRetries = N` we perform up to `N + 1` rawFetch calls and at
5981
+ * most `N` approval rounds. The `attempt === maxRetries` guard ensures we
5982
+ * don't run an (N+1)th approval round we'll never consume — if the final
5983
+ * retry still returns `approval_required`, we break out and throw
5984
+ * `max_retries_exceeded` instead.
5985
+ */
5986
+ this.fetch = async (path, init) => {
5987
+ const maxRetries = this.options.maxApprovalRetries ?? DEFAULT_MAX_APPROVAL_RETRIES;
5988
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
5989
+ const response = await this.rawFetch(path, init);
5990
+ if (response.status !== 403) return response;
5991
+ const errorType = response.headers.get("x-zapier-error-type");
5992
+ if (errorType === "request_denied_by_policy") {
5993
+ const { data } = await this.parseResult(response);
5994
+ const { message, errors } = this.parseErrorResponse({
5995
+ status: response.status,
5996
+ statusText: response.statusText,
5997
+ data
5799
5998
  });
5800
- await sleep(delayMs);
5801
- continue;
5999
+ throw new ZapierApprovalError(
6000
+ message || "Request explicitly denied by policy",
6001
+ {
6002
+ status: "policy_denied",
6003
+ statusCode: response.status,
6004
+ errors
6005
+ }
6006
+ );
6007
+ }
6008
+ if (errorType !== "approval_required") return response;
6009
+ if (attempt === maxRetries) break;
6010
+ const isInteractive = this.options.isInteractive ?? getZapierIsInteractive();
6011
+ if (!isInteractive) {
6012
+ throw new ZapierApprovalError(
6013
+ "Approval required but session is not interactive",
6014
+ { status: "approval_required" }
6015
+ );
6016
+ }
6017
+ if (!init?.approvalContext) {
6018
+ throw new ZapierApiError(
6019
+ `Received 403 approval_required for ${path}, but the caller did not provide an approvalContext builder. Every approval-capable request must pass approvalContext so the SDK can create the approval with the correct policy context.`,
6020
+ { statusCode: 403 }
6021
+ );
5802
6022
  }
5803
- return response;
6023
+ await this.runOneApprovalRound(init.approvalContext);
5804
6024
  }
6025
+ throw new ZapierApprovalError(
6026
+ `Exceeded maximum approval retries (${maxRetries}) for ${path}`,
6027
+ { status: "max_retries_exceeded" }
6028
+ );
5805
6029
  };
5806
6030
  this.get = async (path, options = {}) => {
5807
6031
  return this.fetchJson("GET", path, void 0, options);
@@ -6086,6 +6310,156 @@ var ZapierApiClient = class {
6086
6310
  }
6087
6311
  return result;
6088
6312
  }
6313
+ /**
6314
+ * Run a single approval round: create the approval, open the URL (poll mode)
6315
+ * or throw (fail mode), poll until resolved, and emit events. Throws on
6316
+ * denied/timeout/unexpected status. Returns on approved.
6317
+ */
6318
+ async runOneApprovalRound(buildContext) {
6319
+ const context = buildContext();
6320
+ let approvalResponse;
6321
+ try {
6322
+ approvalResponse = await this.rawFetch("/api/v0/approvals", {
6323
+ method: "POST",
6324
+ headers: {
6325
+ "Content-Type": "application/json",
6326
+ Accept: "application/json"
6327
+ },
6328
+ body: JSON.stringify({ context })
6329
+ });
6330
+ } catch (err) {
6331
+ throw new ZapierApiError("Failed to create approval request", {
6332
+ statusCode: 0,
6333
+ cause: err
6334
+ });
6335
+ }
6336
+ if (!approvalResponse.ok) {
6337
+ const body2 = await approvalResponse.text().catch(() => void 0);
6338
+ throw new ZapierApiError(
6339
+ `Failed to create approval request: ${approvalResponse.status}`,
6340
+ { statusCode: approvalResponse.status, response: body2 }
6341
+ );
6342
+ }
6343
+ let body;
6344
+ try {
6345
+ body = await approvalResponse.text();
6346
+ } catch (err) {
6347
+ throw new ZapierApiError("Failed to read approval response body", {
6348
+ statusCode: approvalResponse.status,
6349
+ cause: err
6350
+ });
6351
+ }
6352
+ let approval;
6353
+ try {
6354
+ approval = CreateApprovalResponseSchema.parse(JSON.parse(body));
6355
+ } catch (err) {
6356
+ throw new ZapierApiError(`Failed to parse approval response: ${body}`, {
6357
+ statusCode: approvalResponse.status,
6358
+ cause: err,
6359
+ response: body
6360
+ });
6361
+ }
6362
+ const sdkapiOrigin = new URL(this.buildUrl("/api/v0/approvals").url).origin;
6363
+ const browserOrigin = getZapierBaseUrl(this.options.baseUrl) ?? sdkapiOrigin;
6364
+ const assertApprovalOrigin = (url, expectedOrigin, label) => {
6365
+ let parsed;
6366
+ try {
6367
+ parsed = new URL(url);
6368
+ } catch {
6369
+ throw new ZapierApiError(`Invalid approval ${label}: ${url}`, {
6370
+ statusCode: approvalResponse.status,
6371
+ response: body
6372
+ });
6373
+ }
6374
+ if (parsed.origin !== expectedOrigin) {
6375
+ throw new ZapierApiError(
6376
+ `Approval ${label} origin ${parsed.origin} does not match expected ${expectedOrigin}`,
6377
+ { statusCode: approvalResponse.status, response: body }
6378
+ );
6379
+ }
6380
+ };
6381
+ assertApprovalOrigin(approval.poll_url, sdkapiOrigin, "poll_url");
6382
+ assertApprovalOrigin(approval.approval_url, browserOrigin, "approval_url");
6383
+ this.emitEvent("approval:required", {
6384
+ approvalId: approval.approval_id,
6385
+ approvalUrl: approval.approval_url
6386
+ });
6387
+ const approvalMode = this.options.approvalMode ?? getZapierApprovalMode();
6388
+ if (approvalMode === "fail") {
6389
+ throw new ZapierApprovalError("This request requires approval.", {
6390
+ approvalId: approval.approval_id,
6391
+ approvalUrl: approval.approval_url,
6392
+ pollUrl: approval.poll_url,
6393
+ status: "pending"
6394
+ });
6395
+ }
6396
+ await openApproval(approval.approval_url);
6397
+ const timeoutMs = this.options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
6398
+ let rawPollResult;
6399
+ try {
6400
+ rawPollResult = await pollUntilComplete({
6401
+ // poll_url is an absolute URL supplied by the server, so we use
6402
+ // rawFetchUrl directly (skipping path resolution) but still share
6403
+ // auth + interactive-header + 429-retry with the rest of the SDK.
6404
+ fetchPoll: () => this.rawFetchUrl(approval.poll_url, {
6405
+ method: "GET",
6406
+ headers: { Accept: "application/json" }
6407
+ }),
6408
+ timeoutMs,
6409
+ isPending: (body2) => {
6410
+ const parsed = PollApprovalResponseSchema.safeParse(body2);
6411
+ return parsed.success && parsed.data.status === "pending_approval";
6412
+ }
6413
+ });
6414
+ } catch (err) {
6415
+ if (!(err instanceof ZapierTimeoutError)) {
6416
+ throw err;
6417
+ }
6418
+ this.emitEvent("approval:timeout", {
6419
+ approvalId: approval.approval_id
6420
+ });
6421
+ throw new ZapierApprovalError(
6422
+ `Approval timed out after ${timeoutMs / 1e3} seconds`,
6423
+ {
6424
+ approvalId: approval.approval_id,
6425
+ approvalUrl: approval.approval_url,
6426
+ pollUrl: approval.poll_url,
6427
+ status: "timeout",
6428
+ cause: err
6429
+ }
6430
+ );
6431
+ }
6432
+ const pollParse = PollApprovalResponseSchema.safeParse(rawPollResult);
6433
+ if (!pollParse.success) {
6434
+ const bodyPreview = typeof rawPollResult === "string" ? rawPollResult : JSON.stringify(rawPollResult);
6435
+ throw new ZapierApiError(
6436
+ `Failed to parse approval poll response: ${bodyPreview}`,
6437
+ {
6438
+ statusCode: 0,
6439
+ cause: pollParse.error,
6440
+ response: rawPollResult
6441
+ }
6442
+ );
6443
+ }
6444
+ const pollResult = pollParse.data;
6445
+ if (pollResult.status === "denied") {
6446
+ this.emitEvent("approval:denied", {
6447
+ approvalId: approval.approval_id
6448
+ });
6449
+ throw new ZapierApprovalError("Request denied by user", {
6450
+ approvalId: approval.approval_id,
6451
+ status: "denied"
6452
+ });
6453
+ }
6454
+ if (pollResult.status !== "approved") {
6455
+ throw new ZapierApiError(
6456
+ `Unexpected approval status received: ${pollResult.status}`
6457
+ );
6458
+ }
6459
+ this.emitEvent("approval:approved", {
6460
+ approvalId: approval.approval_id
6461
+ });
6462
+ }
6089
6463
  };
6090
6464
  var createZapierApi = (options) => {
6091
6465
  const { debug = false, fetch: originalFetch = globalThis.fetch } = options;
@@ -6108,7 +6482,11 @@ var apiPlugin = (sdk) => {
6108
6482
  onEvent,
6109
6483
  debug = false,
6110
6484
  maxNetworkRetries = ZAPIER_MAX_NETWORK_RETRIES,
6111
- maxNetworkRetryDelayMs = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS
6485
+ maxNetworkRetryDelayMs = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS,
6486
+ isInteractive,
6487
+ approvalTimeoutMs,
6488
+ maxApprovalRetries,
6489
+ approvalMode
6112
6490
  } = sdk.context.options;
6113
6491
  const api = createZapierApi({
6114
6492
  baseUrl,
@@ -6118,7 +6496,11 @@ var apiPlugin = (sdk) => {
6118
6496
  fetch: customFetch,
6119
6497
  onEvent,
6120
6498
  maxNetworkRetries,
6121
- maxNetworkRetryDelayMs
6499
+ maxNetworkRetryDelayMs,
6500
+ isInteractive,
6501
+ approvalTimeoutMs,
6502
+ maxApprovalRetries,
6503
+ approvalMode
6122
6504
  });
6123
6505
  return {
6124
6506
  context: {
@@ -7673,7 +8055,7 @@ var findUniqueAuthenticationPlugin = (sdk) => ({
7673
8055
  var GetInputFieldsSchemaDescription = "Get the JSON Schema representation of input fields for an action. Returns a JSON Schema object describing the structure, types, and validation rules for the action's input parameters.";
7674
8056
  var GetInputFieldsSchemaBaseSchema = zod.z.object({
7675
8057
  connection: ConnectionPropertySchema.optional().describe(
7676
- "Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly. Mutually exclusive with connectionId."
8058
+ "Connection alias or connection ID (UUID or positive integer). Strings that match a key in the connections map are resolved against it; otherwise the value is used as a connection ID directly. Mutually exclusive with connectionId."
7677
8059
  ),
7678
8060
  connectionId: ConnectionIdPropertySchema.nullable().optional().describe(
7679
8061
  "Connection ID to use when fetching the schema. Required if the action needs a connection to determine available fields."
@@ -7811,7 +8193,7 @@ var InputFieldChoiceItemSchema = withFormatter(NeedChoicesSchema, {
7811
8193
  var ListInputFieldChoicesDescription = "Get the available choices for a dynamic dropdown input field";
7812
8194
  var ListInputFieldChoicesBaseSchema = zod.z.object({
7813
8195
  connection: ConnectionPropertySchema.optional().describe(
7814
- "Connection alias (string) or numeric connectionId. Strings are resolved from the connections map; numbers are used directly. Mutually exclusive with connectionId."
8196
+ "Connection alias or connection ID (UUID or positive integer). Strings that match a key in the connections map are resolved against it; otherwise the value is used as a connection ID directly. Mutually exclusive with connectionId."
7815
8197
  ),
7816
8198
  connectionId: ConnectionIdPropertySchema.nullable().optional().describe(
7817
8199
  "Connection ID to use when listing available field choices. Required if the action needs a connection to populate dynamic dropdown options."
@@ -8670,6 +9052,16 @@ var BaseSdkOptionsSchema = zod.z.object({
8670
9052
  * Default is 60000 (60 seconds).
8671
9053
  */
8672
9054
  maxNetworkRetryDelayMs: zod.z.number().optional().describe("Max delay in ms to wait for retry (default: 60000).").meta({ valueHint: "ms" }),
9055
+ isInteractive: zod.z.boolean().optional().describe(
9056
+ "Whether this session is interactive (user can visit approval URLs). Defaults to ZAPIER_IS_INTERACTIVE env var."
9057
+ ).meta({ internal: true }),
9058
+ approvalTimeoutMs: zod.z.number().optional().describe("Timeout in ms for approval polling. Default: 600000 (10 min).").meta({ valueHint: "ms", internal: true }),
9059
+ maxApprovalRetries: zod.z.number().optional().describe(
9060
+ "Maximum number of sequential approval rounds per request (one per gating policy) before giving up. Default: 2."
9061
+ ).meta({ internal: true }),
9062
+ approvalMode: zod.z.enum(["poll", "fail"]).optional().describe(
9063
+ 'Approval flow behavior. "poll" opens browser and polls (default). "fail" creates the approval and throws immediately with the approval URL.'
9064
+ ).meta({ internal: true }),
8673
9065
  // Internal
8674
9066
  manifestPath: zod.z.string().optional().describe("Path to a .zapierrc manifest file for app version locking.").meta({ internal: true }),
8675
9067
  manifest: zod.z.custom().optional().describe("Manifest for app version locking.").meta({ internal: true }),
@@ -8705,7 +9097,9 @@ exports.CredentialsFunctionSchema = CredentialsFunctionSchema;
8705
9097
  exports.CredentialsObjectSchema = CredentialsObjectSchema;
8706
9098
  exports.CredentialsSchema = CredentialsSchema;
8707
9099
  exports.DEFAULT_ACTION_TIMEOUT_MS = DEFAULT_ACTION_TIMEOUT_MS;
9100
+ exports.DEFAULT_APPROVAL_TIMEOUT_MS = DEFAULT_APPROVAL_TIMEOUT_MS;
8708
9101
  exports.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH;
9102
+ exports.DEFAULT_MAX_APPROVAL_RETRIES = DEFAULT_MAX_APPROVAL_RETRIES;
8709
9103
  exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
8710
9104
  exports.DebugPropertySchema = DebugPropertySchema;
8711
9105
  exports.FieldsPropertySchema = FieldsPropertySchema;
@@ -8730,6 +9124,7 @@ exports.ZAPIER_MAX_NETWORK_RETRY_DELAY_MS = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS;
8730
9124
  exports.ZapierActionError = ZapierActionError;
8731
9125
  exports.ZapierApiError = ZapierApiError;
8732
9126
  exports.ZapierAppNotFoundError = ZapierAppNotFoundError;
9127
+ exports.ZapierApprovalError = ZapierApprovalError;
8733
9128
  exports.ZapierAuthenticationError = ZapierAuthenticationError;
8734
9129
  exports.ZapierBundleError = ZapierBundleError;
8735
9130
  exports.ZapierConfigurationError = ZapierConfigurationError;
@@ -8797,6 +9192,8 @@ exports.getReleaseId = getReleaseId;
8797
9192
  exports.getTablePlugin = getTablePlugin;
8798
9193
  exports.getTableRecordPlugin = getTableRecordPlugin;
8799
9194
  exports.getTokenFromCliLogin = getTokenFromCliLogin;
9195
+ exports.getZapierApprovalMode = getZapierApprovalMode;
9196
+ exports.getZapierIsInteractive = getZapierIsInteractive;
8800
9197
  exports.injectCliLogin = injectCliLogin;
8801
9198
  exports.inputFieldKeyResolver = inputFieldKeyResolver;
8802
9199
  exports.inputsAllOptionalResolver = inputsAllOptionalResolver;