@zapier/zapier-sdk 0.42.1 → 0.43.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 +6 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +309 -35
- package/dist/api/types.d.ts +29 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/constants.d.ts +25 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +32 -0
- package/dist/index.cjs +431 -38
- package/dist/index.d.mts +79 -1
- package/dist/index.mjs +427 -39
- package/dist/plugins/api/index.d.ts.map +1 -1
- package/dist/plugins/api/index.js +5 -1
- package/dist/plugins/createClientCredentials/index.d.ts.map +1 -1
- package/dist/plugins/createClientCredentials/index.js +1 -0
- package/dist/plugins/createClientCredentials/schemas.d.ts +1 -0
- package/dist/plugins/createClientCredentials/schemas.d.ts.map +1 -1
- package/dist/plugins/createClientCredentials/schemas.js +6 -0
- package/dist/plugins/fetch/index.d.ts.map +1 -1
- package/dist/plugins/fetch/index.js +15 -1
- package/dist/plugins/runAction/index.d.ts.map +1 -1
- package/dist/plugins/runAction/index.js +12 -1
- package/dist/types/errors.d.ts +37 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +18 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/sdk.d.ts.map +1 -1
- package/dist/types/sdk.js +20 -0
- package/dist/utils/open-approval.d.ts +2 -0
- package/dist/utils/open-approval.d.ts.map +1 -0
- package/dist/utils/open-approval.js +13 -0
- package/dist/utils/open-url.d.ts +3 -0
- package/dist/utils/open-url.d.ts.map +1 -0
- package/dist/utils/open-url.js +72 -0
- 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(
|
|
@@ -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
|
|
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) {
|
|
@@ -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 }) => {
|
|
@@ -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}`, {
|
|
@@ -5689,7 +5736,89 @@ async function invalidateCredentialsToken(options) {
|
|
|
5689
5736
|
}
|
|
5690
5737
|
}
|
|
5691
5738
|
|
|
5692
|
-
// src/
|
|
5739
|
+
// src/utils/open-url.ts
|
|
5740
|
+
var nodePrefix = "node:";
|
|
5741
|
+
async function loadChildProcess() {
|
|
5742
|
+
return import(`${nodePrefix}child_process`);
|
|
5743
|
+
}
|
|
5744
|
+
async function loadProcess() {
|
|
5745
|
+
return import(`${nodePrefix}process`);
|
|
5746
|
+
}
|
|
5747
|
+
var openUrl = async (url) => {
|
|
5748
|
+
if (typeof url !== "string") {
|
|
5749
|
+
throw new TypeError("Expected `url` to be a string");
|
|
5750
|
+
}
|
|
5751
|
+
let parsed;
|
|
5752
|
+
try {
|
|
5753
|
+
parsed = new URL(url);
|
|
5754
|
+
} catch {
|
|
5755
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
5756
|
+
}
|
|
5757
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
5758
|
+
throw new Error(`Refusing to open non-http(s) URL: ${parsed.protocol}`);
|
|
5759
|
+
}
|
|
5760
|
+
const target = parsed.toString();
|
|
5761
|
+
if (typeof globalThis.window !== "undefined" && typeof globalThis.window.open === "function") {
|
|
5762
|
+
globalThis.window.open(target, "_blank", "noopener");
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
let spawn;
|
|
5766
|
+
let platform;
|
|
5767
|
+
try {
|
|
5768
|
+
const [cp, proc] = await Promise.all([loadChildProcess(), loadProcess()]);
|
|
5769
|
+
spawn = cp.spawn;
|
|
5770
|
+
platform = proc.platform;
|
|
5771
|
+
} catch {
|
|
5772
|
+
throw new Error(
|
|
5773
|
+
"openUrl: no supported runtime. window.open is unavailable and node:child_process could not be loaded."
|
|
5774
|
+
);
|
|
5775
|
+
}
|
|
5776
|
+
if (typeof spawn !== "function") {
|
|
5777
|
+
throw new Error(
|
|
5778
|
+
"openUrl: child_process.spawn is not available in this runtime"
|
|
5779
|
+
);
|
|
5780
|
+
}
|
|
5781
|
+
let command;
|
|
5782
|
+
let args;
|
|
5783
|
+
if (platform === "darwin") {
|
|
5784
|
+
command = "open";
|
|
5785
|
+
args = [target];
|
|
5786
|
+
} else if (platform === "win32") {
|
|
5787
|
+
command = "rundll32";
|
|
5788
|
+
args = ["url.dll,FileProtocolHandler", target];
|
|
5789
|
+
} else {
|
|
5790
|
+
throw new Error(`Unsupported platform: ${platform}`);
|
|
5791
|
+
}
|
|
5792
|
+
const child = spawn(command, args, {
|
|
5793
|
+
stdio: "ignore",
|
|
5794
|
+
detached: true,
|
|
5795
|
+
windowsHide: true
|
|
5796
|
+
});
|
|
5797
|
+
child.once("error", () => {
|
|
5798
|
+
});
|
|
5799
|
+
child.unref();
|
|
5800
|
+
};
|
|
5801
|
+
var open_url_default = openUrl;
|
|
5802
|
+
|
|
5803
|
+
// src/utils/open-approval.ts
|
|
5804
|
+
async function openApproval(url) {
|
|
5805
|
+
console.error(`Approval required. Opening browser: ${url}`);
|
|
5806
|
+
try {
|
|
5807
|
+
await open_url_default(url);
|
|
5808
|
+
} catch {
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
var ApprovalStatusSchema = zod.z.enum(["pending_approval", "approved", "denied"]);
|
|
5812
|
+
var CreateApprovalResponseSchema = zod.z.object({
|
|
5813
|
+
status: ApprovalStatusSchema,
|
|
5814
|
+
approval_id: zod.z.string(),
|
|
5815
|
+
approval_url: zod.z.string().url(),
|
|
5816
|
+
poll_url: zod.z.string().url()
|
|
5817
|
+
});
|
|
5818
|
+
var PollApprovalResponseSchema = zod.z.object({
|
|
5819
|
+
status: ApprovalStatusSchema,
|
|
5820
|
+
approval_id: zod.z.string()
|
|
5821
|
+
});
|
|
5693
5822
|
function parseRateLimitHeaders(response) {
|
|
5694
5823
|
const info = {};
|
|
5695
5824
|
const retryAfter = response.headers.get("retry-after");
|
|
@@ -5750,16 +5879,23 @@ var pathConfig = {
|
|
|
5750
5879
|
var ZapierApiClient = class {
|
|
5751
5880
|
constructor(options) {
|
|
5752
5881
|
this.options = options;
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5882
|
+
/**
|
|
5883
|
+
* Perform a request against an already-resolved URL.
|
|
5884
|
+
*
|
|
5885
|
+
* Does auth, header merging, and 429 retry — all the cross-cutting
|
|
5886
|
+
* concerns that every Zapier-bound HTTP call needs. Callers that have a
|
|
5887
|
+
* path (e.g. `/relay/...`) should use `rawFetch` instead, which does
|
|
5888
|
+
* path → URL resolution and delegates here.
|
|
5889
|
+
*
|
|
5890
|
+
* Exposed as a separate helper so call sites with a server-supplied
|
|
5891
|
+
* absolute URL (e.g. an approval poll URL) can still share the same
|
|
5892
|
+
* auth/retry pipeline instead of reaching for `this.options.fetch`
|
|
5893
|
+
* directly and drifting.
|
|
5894
|
+
*/
|
|
5895
|
+
this.rawFetchUrl = async (url, init, pathConfig2) => {
|
|
5759
5896
|
if (init?.body && (isPlainObject(init.body) || Array.isArray(init.body))) {
|
|
5760
5897
|
init.body = JSON.stringify(init.body);
|
|
5761
5898
|
}
|
|
5762
|
-
const { url, pathConfig: pathConfig2 } = this.buildUrl(path, init?.searchParams);
|
|
5763
5899
|
const builtHeaders = await this.buildHeaders(
|
|
5764
5900
|
init,
|
|
5765
5901
|
pathConfig2
|
|
@@ -5778,30 +5914,114 @@ var ZapierApiClient = class {
|
|
|
5778
5914
|
...init,
|
|
5779
5915
|
headers: mergedHeaders
|
|
5780
5916
|
});
|
|
5781
|
-
if (response.status
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5917
|
+
if (response.status !== 429) {
|
|
5918
|
+
return response;
|
|
5919
|
+
}
|
|
5920
|
+
const rateLimitInfo = parseRateLimitHeaders(response);
|
|
5921
|
+
const delayMs = rateLimitInfo.retryAfterMs ?? calculateExponentialBackoffMs(retries + 1);
|
|
5922
|
+
if (delayMs > this.maxNetworkRetryDelayMs || retries >= this.maxNetworkRetries) {
|
|
5923
|
+
throw new ZapierRateLimitError("Rate limited", {
|
|
5924
|
+
statusCode: 429,
|
|
5925
|
+
rateLimit: rateLimitInfo,
|
|
5926
|
+
retries
|
|
5927
|
+
});
|
|
5928
|
+
}
|
|
5929
|
+
retries++;
|
|
5930
|
+
this.emitEvent("api:rate_limit_retry", {
|
|
5931
|
+
retry: retries,
|
|
5932
|
+
maxNetworkRetries: this.maxNetworkRetries,
|
|
5933
|
+
delayMs,
|
|
5934
|
+
path: url,
|
|
5935
|
+
method: init?.method ?? "GET",
|
|
5936
|
+
rateLimit: rateLimitInfo
|
|
5937
|
+
});
|
|
5938
|
+
await sleep(delayMs);
|
|
5939
|
+
}
|
|
5940
|
+
};
|
|
5941
|
+
/**
|
|
5942
|
+
* Perform a request with auth, header merging, and rate-limit (429) retries.
|
|
5943
|
+
* Does NOT handle 403 approval_required — that's routed by `fetch`.
|
|
5944
|
+
*/
|
|
5945
|
+
this.rawFetch = async (path, init) => {
|
|
5946
|
+
if (!path.startsWith("/")) {
|
|
5947
|
+
throw new ZapierValidationError(
|
|
5948
|
+
`fetch expects a path starting with '/', got: ${path}`
|
|
5949
|
+
);
|
|
5950
|
+
}
|
|
5951
|
+
const { url, pathConfig: pathConfig2 } = this.buildUrl(path, init?.searchParams);
|
|
5952
|
+
return this.rawFetchUrl(url, init, pathConfig2);
|
|
5953
|
+
};
|
|
5954
|
+
/**
|
|
5955
|
+
* Approval-aware HTTP fetch.
|
|
5956
|
+
*
|
|
5957
|
+
* Wraps `rawFetch` with the backend's just-in-time approval protocol. The
|
|
5958
|
+
* backend signals approval state via a 403 response with an
|
|
5959
|
+
* `x-zapier-error-type` header:
|
|
5960
|
+
*
|
|
5961
|
+
* - `request_denied_by_policy` → a policy rule permanently blocks the
|
|
5962
|
+
* request; no human can approve it. Throw.
|
|
5963
|
+
* - `approval_required` → the request needs human approval; the
|
|
5964
|
+
* SDK creates an approval, opens the URL
|
|
5965
|
+
* (poll mode), waits for resolution, and
|
|
5966
|
+
* retries the original request.
|
|
5967
|
+
* - anything else → not our concern, pass through.
|
|
5968
|
+
*
|
|
5969
|
+
* The retry loop exists because a single user action can legitimately
|
|
5970
|
+
* require multiple sequential approvals (e.g. policies that approve one
|
|
5971
|
+
* step at a time). Each iteration is either a first attempt or a post-
|
|
5972
|
+
* approval retry; `maxApprovalRetries` bounds the loop as a runaway-loop
|
|
5973
|
+
* safeguard.
|
|
5974
|
+
*
|
|
5975
|
+
* Loop accounting: `attempt` counts rawFetch calls, not approval rounds.
|
|
5976
|
+
* With `maxRetries = N` we perform up to `N + 1` rawFetch calls and at
|
|
5977
|
+
* most `N` approval rounds. The `attempt === maxRetries` guard ensures we
|
|
5978
|
+
* don't run an (N+1)th approval round we'll never consume — if the final
|
|
5979
|
+
* retry still returns `approval_required`, we break out and throw
|
|
5980
|
+
* `max_retries_exceeded` instead.
|
|
5981
|
+
*/
|
|
5982
|
+
this.fetch = async (path, init) => {
|
|
5983
|
+
const maxRetries = this.options.maxApprovalRetries ?? DEFAULT_MAX_APPROVAL_RETRIES;
|
|
5984
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
5985
|
+
const response = await this.rawFetch(path, init);
|
|
5986
|
+
if (response.status !== 403) return response;
|
|
5987
|
+
const errorType = response.headers.get("x-zapier-error-type");
|
|
5988
|
+
if (errorType === "request_denied_by_policy") {
|
|
5989
|
+
const { data } = await this.parseResult(response);
|
|
5990
|
+
const { message, errors } = this.parseErrorResponse({
|
|
5991
|
+
status: response.status,
|
|
5992
|
+
statusText: response.statusText,
|
|
5993
|
+
data
|
|
5799
5994
|
});
|
|
5800
|
-
|
|
5801
|
-
|
|
5995
|
+
throw new ZapierApprovalError(
|
|
5996
|
+
message || "Request explicitly denied by policy",
|
|
5997
|
+
{
|
|
5998
|
+
status: "policy_denied",
|
|
5999
|
+
statusCode: response.status,
|
|
6000
|
+
errors
|
|
6001
|
+
}
|
|
6002
|
+
);
|
|
6003
|
+
}
|
|
6004
|
+
if (errorType !== "approval_required") return response;
|
|
6005
|
+
if (attempt === maxRetries) break;
|
|
6006
|
+
const isInteractive = this.options.isInteractive ?? getZapierIsInteractive();
|
|
6007
|
+
if (!isInteractive) {
|
|
6008
|
+
throw new ZapierApprovalError(
|
|
6009
|
+
"Approval required but session is not interactive",
|
|
6010
|
+
{ status: "approval_required" }
|
|
6011
|
+
);
|
|
6012
|
+
}
|
|
6013
|
+
if (!init?.approvalContext) {
|
|
6014
|
+
throw new ZapierApiError(
|
|
6015
|
+
`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.`,
|
|
6016
|
+
{ statusCode: 403 }
|
|
6017
|
+
);
|
|
5802
6018
|
}
|
|
5803
|
-
|
|
6019
|
+
await this.runOneApprovalRound(init.approvalContext);
|
|
5804
6020
|
}
|
|
6021
|
+
throw new ZapierApprovalError(
|
|
6022
|
+
`Exceeded maximum approval retries (${maxRetries}) for ${path}`,
|
|
6023
|
+
{ status: "max_retries_exceeded" }
|
|
6024
|
+
);
|
|
5805
6025
|
};
|
|
5806
6026
|
this.get = async (path, options = {}) => {
|
|
5807
6027
|
return this.fetchJson("GET", path, void 0, options);
|
|
@@ -6086,6 +6306,156 @@ var ZapierApiClient = class {
|
|
|
6086
6306
|
}
|
|
6087
6307
|
return result;
|
|
6088
6308
|
}
|
|
6309
|
+
/**
|
|
6310
|
+
* Run a single approval round: create the approval, open the URL (poll mode)
|
|
6311
|
+
* or throw (fail mode), poll until resolved, and emit events. Throws on
|
|
6312
|
+
* denied/timeout/unexpected status. Returns on approved.
|
|
6313
|
+
*/
|
|
6314
|
+
async runOneApprovalRound(buildContext) {
|
|
6315
|
+
const context = buildContext();
|
|
6316
|
+
let approvalResponse;
|
|
6317
|
+
try {
|
|
6318
|
+
approvalResponse = await this.rawFetch("/api/v0/approvals", {
|
|
6319
|
+
method: "POST",
|
|
6320
|
+
headers: {
|
|
6321
|
+
"Content-Type": "application/json",
|
|
6322
|
+
Accept: "application/json"
|
|
6323
|
+
},
|
|
6324
|
+
body: JSON.stringify({ context })
|
|
6325
|
+
});
|
|
6326
|
+
} catch (err) {
|
|
6327
|
+
throw new ZapierApiError("Failed to create approval request", {
|
|
6328
|
+
statusCode: 0,
|
|
6329
|
+
cause: err
|
|
6330
|
+
});
|
|
6331
|
+
}
|
|
6332
|
+
if (!approvalResponse.ok) {
|
|
6333
|
+
const body2 = await approvalResponse.text().catch(() => void 0);
|
|
6334
|
+
throw new ZapierApiError(
|
|
6335
|
+
`Failed to create approval request: ${approvalResponse.status}`,
|
|
6336
|
+
{ statusCode: approvalResponse.status, response: body2 }
|
|
6337
|
+
);
|
|
6338
|
+
}
|
|
6339
|
+
let body;
|
|
6340
|
+
try {
|
|
6341
|
+
body = await approvalResponse.text();
|
|
6342
|
+
} catch (err) {
|
|
6343
|
+
throw new ZapierApiError("Failed to read approval response body", {
|
|
6344
|
+
statusCode: approvalResponse.status,
|
|
6345
|
+
cause: err
|
|
6346
|
+
});
|
|
6347
|
+
}
|
|
6348
|
+
let approval;
|
|
6349
|
+
try {
|
|
6350
|
+
approval = CreateApprovalResponseSchema.parse(JSON.parse(body));
|
|
6351
|
+
} catch (err) {
|
|
6352
|
+
throw new ZapierApiError(`Failed to parse approval response: ${body}`, {
|
|
6353
|
+
statusCode: approvalResponse.status,
|
|
6354
|
+
cause: err,
|
|
6355
|
+
response: body
|
|
6356
|
+
});
|
|
6357
|
+
}
|
|
6358
|
+
const sdkapiOrigin = new URL(this.buildUrl("/api/v0/approvals").url).origin;
|
|
6359
|
+
const browserOrigin = getZapierBaseUrl(this.options.baseUrl) ?? sdkapiOrigin;
|
|
6360
|
+
const assertApprovalOrigin = (url, expectedOrigin, label) => {
|
|
6361
|
+
let parsed;
|
|
6362
|
+
try {
|
|
6363
|
+
parsed = new URL(url);
|
|
6364
|
+
} catch {
|
|
6365
|
+
throw new ZapierApiError(`Invalid approval ${label}: ${url}`, {
|
|
6366
|
+
statusCode: approvalResponse.status,
|
|
6367
|
+
response: body
|
|
6368
|
+
});
|
|
6369
|
+
}
|
|
6370
|
+
if (parsed.origin !== expectedOrigin) {
|
|
6371
|
+
throw new ZapierApiError(
|
|
6372
|
+
`Approval ${label} origin ${parsed.origin} does not match expected ${expectedOrigin}`,
|
|
6373
|
+
{ statusCode: approvalResponse.status, response: body }
|
|
6374
|
+
);
|
|
6375
|
+
}
|
|
6376
|
+
};
|
|
6377
|
+
assertApprovalOrigin(approval.poll_url, sdkapiOrigin, "poll_url");
|
|
6378
|
+
assertApprovalOrigin(approval.approval_url, browserOrigin, "approval_url");
|
|
6379
|
+
this.emitEvent("approval:required", {
|
|
6380
|
+
approvalId: approval.approval_id,
|
|
6381
|
+
approvalUrl: approval.approval_url
|
|
6382
|
+
});
|
|
6383
|
+
const approvalMode = this.options.approvalMode ?? getZapierApprovalMode();
|
|
6384
|
+
if (approvalMode === "fail") {
|
|
6385
|
+
throw new ZapierApprovalError("This request requires approval.", {
|
|
6386
|
+
approvalId: approval.approval_id,
|
|
6387
|
+
approvalUrl: approval.approval_url,
|
|
6388
|
+
pollUrl: approval.poll_url,
|
|
6389
|
+
status: "pending"
|
|
6390
|
+
});
|
|
6391
|
+
}
|
|
6392
|
+
await openApproval(approval.approval_url);
|
|
6393
|
+
const timeoutMs = this.options.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
6394
|
+
let rawPollResult;
|
|
6395
|
+
try {
|
|
6396
|
+
rawPollResult = await pollUntilComplete({
|
|
6397
|
+
// poll_url is an absolute URL supplied by the server, so we use
|
|
6398
|
+
// rawFetchUrl directly (skipping path resolution) but still share
|
|
6399
|
+
// auth + interactive-header + 429-retry with the rest of the SDK.
|
|
6400
|
+
fetchPoll: () => this.rawFetchUrl(approval.poll_url, {
|
|
6401
|
+
method: "GET",
|
|
6402
|
+
headers: { Accept: "application/json" }
|
|
6403
|
+
}),
|
|
6404
|
+
timeoutMs,
|
|
6405
|
+
isPending: (body2) => {
|
|
6406
|
+
const parsed = PollApprovalResponseSchema.safeParse(body2);
|
|
6407
|
+
return parsed.success && parsed.data.status === "pending_approval";
|
|
6408
|
+
}
|
|
6409
|
+
});
|
|
6410
|
+
} catch (err) {
|
|
6411
|
+
if (!(err instanceof ZapierTimeoutError)) {
|
|
6412
|
+
throw err;
|
|
6413
|
+
}
|
|
6414
|
+
this.emitEvent("approval:timeout", {
|
|
6415
|
+
approvalId: approval.approval_id
|
|
6416
|
+
});
|
|
6417
|
+
throw new ZapierApprovalError(
|
|
6418
|
+
`Approval timed out after ${timeoutMs / 1e3} seconds`,
|
|
6419
|
+
{
|
|
6420
|
+
approvalId: approval.approval_id,
|
|
6421
|
+
approvalUrl: approval.approval_url,
|
|
6422
|
+
pollUrl: approval.poll_url,
|
|
6423
|
+
status: "timeout",
|
|
6424
|
+
cause: err
|
|
6425
|
+
}
|
|
6426
|
+
);
|
|
6427
|
+
}
|
|
6428
|
+
const pollParse = PollApprovalResponseSchema.safeParse(rawPollResult);
|
|
6429
|
+
if (!pollParse.success) {
|
|
6430
|
+
const bodyPreview = typeof rawPollResult === "string" ? rawPollResult : JSON.stringify(rawPollResult);
|
|
6431
|
+
throw new ZapierApiError(
|
|
6432
|
+
`Failed to parse approval poll response: ${bodyPreview}`,
|
|
6433
|
+
{
|
|
6434
|
+
statusCode: 0,
|
|
6435
|
+
cause: pollParse.error,
|
|
6436
|
+
response: rawPollResult
|
|
6437
|
+
}
|
|
6438
|
+
);
|
|
6439
|
+
}
|
|
6440
|
+
const pollResult = pollParse.data;
|
|
6441
|
+
if (pollResult.status === "denied") {
|
|
6442
|
+
this.emitEvent("approval:denied", {
|
|
6443
|
+
approvalId: approval.approval_id
|
|
6444
|
+
});
|
|
6445
|
+
throw new ZapierApprovalError("Request denied by user", {
|
|
6446
|
+
approvalId: approval.approval_id,
|
|
6447
|
+
status: "denied"
|
|
6448
|
+
});
|
|
6449
|
+
}
|
|
6450
|
+
if (pollResult.status !== "approved") {
|
|
6451
|
+
throw new ZapierApiError(
|
|
6452
|
+
`Unexpected approval status received: ${pollResult.status}`
|
|
6453
|
+
);
|
|
6454
|
+
}
|
|
6455
|
+
this.emitEvent("approval:approved", {
|
|
6456
|
+
approvalId: approval.approval_id
|
|
6457
|
+
});
|
|
6458
|
+
}
|
|
6089
6459
|
};
|
|
6090
6460
|
var createZapierApi = (options) => {
|
|
6091
6461
|
const { debug = false, fetch: originalFetch = globalThis.fetch } = options;
|
|
@@ -6108,7 +6478,11 @@ var apiPlugin = (sdk) => {
|
|
|
6108
6478
|
onEvent,
|
|
6109
6479
|
debug = false,
|
|
6110
6480
|
maxNetworkRetries = ZAPIER_MAX_NETWORK_RETRIES,
|
|
6111
|
-
maxNetworkRetryDelayMs = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS
|
|
6481
|
+
maxNetworkRetryDelayMs = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS,
|
|
6482
|
+
isInteractive,
|
|
6483
|
+
approvalTimeoutMs,
|
|
6484
|
+
maxApprovalRetries,
|
|
6485
|
+
approvalMode
|
|
6112
6486
|
} = sdk.context.options;
|
|
6113
6487
|
const api = createZapierApi({
|
|
6114
6488
|
baseUrl,
|
|
@@ -6118,7 +6492,11 @@ var apiPlugin = (sdk) => {
|
|
|
6118
6492
|
fetch: customFetch,
|
|
6119
6493
|
onEvent,
|
|
6120
6494
|
maxNetworkRetries,
|
|
6121
|
-
maxNetworkRetryDelayMs
|
|
6495
|
+
maxNetworkRetryDelayMs,
|
|
6496
|
+
isInteractive,
|
|
6497
|
+
approvalTimeoutMs,
|
|
6498
|
+
maxApprovalRetries,
|
|
6499
|
+
approvalMode
|
|
6122
6500
|
});
|
|
6123
6501
|
return {
|
|
6124
6502
|
context: {
|
|
@@ -8670,6 +9048,16 @@ var BaseSdkOptionsSchema = zod.z.object({
|
|
|
8670
9048
|
* Default is 60000 (60 seconds).
|
|
8671
9049
|
*/
|
|
8672
9050
|
maxNetworkRetryDelayMs: zod.z.number().optional().describe("Max delay in ms to wait for retry (default: 60000).").meta({ valueHint: "ms" }),
|
|
9051
|
+
isInteractive: zod.z.boolean().optional().describe(
|
|
9052
|
+
"Whether this session is interactive (user can visit approval URLs). Defaults to ZAPIER_IS_INTERACTIVE env var."
|
|
9053
|
+
).meta({ internal: true }),
|
|
9054
|
+
approvalTimeoutMs: zod.z.number().optional().describe("Timeout in ms for approval polling. Default: 600000 (10 min).").meta({ valueHint: "ms", internal: true }),
|
|
9055
|
+
maxApprovalRetries: zod.z.number().optional().describe(
|
|
9056
|
+
"Maximum number of sequential approval rounds per request (one per gating policy) before giving up. Default: 2."
|
|
9057
|
+
).meta({ internal: true }),
|
|
9058
|
+
approvalMode: zod.z.enum(["poll", "fail"]).optional().describe(
|
|
9059
|
+
'Approval flow behavior. "poll" opens browser and polls (default). "fail" creates the approval and throws immediately with the approval URL.'
|
|
9060
|
+
).meta({ internal: true }),
|
|
8673
9061
|
// Internal
|
|
8674
9062
|
manifestPath: zod.z.string().optional().describe("Path to a .zapierrc manifest file for app version locking.").meta({ internal: true }),
|
|
8675
9063
|
manifest: zod.z.custom().optional().describe("Manifest for app version locking.").meta({ internal: true }),
|
|
@@ -8705,7 +9093,9 @@ exports.CredentialsFunctionSchema = CredentialsFunctionSchema;
|
|
|
8705
9093
|
exports.CredentialsObjectSchema = CredentialsObjectSchema;
|
|
8706
9094
|
exports.CredentialsSchema = CredentialsSchema;
|
|
8707
9095
|
exports.DEFAULT_ACTION_TIMEOUT_MS = DEFAULT_ACTION_TIMEOUT_MS;
|
|
9096
|
+
exports.DEFAULT_APPROVAL_TIMEOUT_MS = DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
8708
9097
|
exports.DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_PATH;
|
|
9098
|
+
exports.DEFAULT_MAX_APPROVAL_RETRIES = DEFAULT_MAX_APPROVAL_RETRIES;
|
|
8709
9099
|
exports.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
8710
9100
|
exports.DebugPropertySchema = DebugPropertySchema;
|
|
8711
9101
|
exports.FieldsPropertySchema = FieldsPropertySchema;
|
|
@@ -8730,6 +9120,7 @@ exports.ZAPIER_MAX_NETWORK_RETRY_DELAY_MS = ZAPIER_MAX_NETWORK_RETRY_DELAY_MS;
|
|
|
8730
9120
|
exports.ZapierActionError = ZapierActionError;
|
|
8731
9121
|
exports.ZapierApiError = ZapierApiError;
|
|
8732
9122
|
exports.ZapierAppNotFoundError = ZapierAppNotFoundError;
|
|
9123
|
+
exports.ZapierApprovalError = ZapierApprovalError;
|
|
8733
9124
|
exports.ZapierAuthenticationError = ZapierAuthenticationError;
|
|
8734
9125
|
exports.ZapierBundleError = ZapierBundleError;
|
|
8735
9126
|
exports.ZapierConfigurationError = ZapierConfigurationError;
|
|
@@ -8797,6 +9188,8 @@ exports.getReleaseId = getReleaseId;
|
|
|
8797
9188
|
exports.getTablePlugin = getTablePlugin;
|
|
8798
9189
|
exports.getTableRecordPlugin = getTableRecordPlugin;
|
|
8799
9190
|
exports.getTokenFromCliLogin = getTokenFromCliLogin;
|
|
9191
|
+
exports.getZapierApprovalMode = getZapierApprovalMode;
|
|
9192
|
+
exports.getZapierIsInteractive = getZapierIsInteractive;
|
|
8800
9193
|
exports.injectCliLogin = injectCliLogin;
|
|
8801
9194
|
exports.inputFieldKeyResolver = inputFieldKeyResolver;
|
|
8802
9195
|
exports.inputsAllOptionalResolver = inputsAllOptionalResolver;
|