@zapier/zapier-sdk-cli 0.52.12 → 0.53.1

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/AGENTS.md +326 -0
  2. package/CHANGELOG.md +22 -0
  3. package/CLAUDE.md +3 -324
  4. package/README.md +20 -0
  5. package/dist/cli.cjs +896 -395
  6. package/dist/cli.mjs +897 -396
  7. package/dist/experimental.cjs +897 -398
  8. package/dist/experimental.d.mts +1 -1
  9. package/dist/experimental.d.ts +1 -1
  10. package/dist/experimental.mjs +896 -397
  11. package/dist/index.cjs +898 -399
  12. package/dist/index.d.mts +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.mjs +897 -398
  15. package/dist/package.json +2 -1
  16. package/dist/{sdk-Sa1HjzUj.d.mts → sdk-SOLizjno.d.mts} +40 -2
  17. package/dist/{sdk-Sa1HjzUj.d.ts → sdk-SOLizjno.d.ts} +40 -2
  18. package/dist/src/experimental.js +2 -1
  19. package/dist/src/plugins/index.d.ts +1 -0
  20. package/dist/src/plugins/index.js +1 -0
  21. package/dist/src/plugins/login/index.d.ts +2 -15
  22. package/dist/src/plugins/login/index.js +3 -191
  23. package/dist/src/plugins/signup/index.d.ts +25 -0
  24. package/dist/src/plugins/signup/index.js +12 -0
  25. package/dist/src/plugins/signup/schemas.d.ts +9 -0
  26. package/dist/src/plugins/signup/schemas.js +26 -0
  27. package/dist/src/plugins/signup/test-harness.d.ts +34 -0
  28. package/dist/src/plugins/signup/test-harness.js +74 -0
  29. package/dist/src/sdk.js +2 -1
  30. package/dist/src/types/sdk.d.ts +2 -1
  31. package/dist/src/utils/auth/account-auth.d.ts +32 -0
  32. package/dist/src/utils/auth/account-auth.js +265 -0
  33. package/dist/src/utils/auth/oauth-callback.d.ts +6 -0
  34. package/dist/src/utils/auth/oauth-callback.js +28 -0
  35. package/dist/src/utils/auth/oauth-errors.d.ts +2 -0
  36. package/dist/src/utils/auth/oauth-errors.js +39 -0
  37. package/dist/src/utils/auth/oauth-flow.d.ts +31 -6
  38. package/dist/src/utils/auth/oauth-flow.js +258 -106
  39. package/dist/src/utils/auth/oauth-transaction.d.ts +35 -0
  40. package/dist/src/utils/auth/oauth-transaction.js +69 -0
  41. package/dist/src/utils/non-interactive.d.ts +5 -4
  42. package/dist/src/utils/non-interactive.js +6 -5
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +4 -3
  45. package/templates/basic/AGENTS.md.hbs +2 -2
@@ -0,0 +1,265 @@
1
+ import { hostname } from "node:os";
2
+ import inquirer from "inquirer";
3
+ import { buildApplicationLifecycleEvent, getOrCreateApiClient, isCredentialsObject, } from "@zapier/zapier-sdk";
4
+ import { revokeCredentials } from "../../login/credentials-revoke";
5
+ import { deleteStoredClientCredentials, getActiveCredentials, } from "../../login/credentials-store";
6
+ import { clearLegacyJwtState, hasLegacyJwtConfig, } from "../../login/legacy-jwt";
7
+ import { resolveCredentialsBaseUrl } from "../../plugins/auth/credentials-base-url";
8
+ import { resolveNonInteractive } from "../non-interactive";
9
+ import { ZapierCliUserCancellationError, ZapierCliValidationError, } from "../errors";
10
+ import { EMPTY_POLICY, setupClientCredentials } from "./client-credentials";
11
+ import { runLoginOauthFlow, runSignupOauthFlow, } from "./oauth-flow";
12
+ import { toRedactedOauthError } from "./oauth-errors";
13
+ const LEGACY_JWT_UPGRADE_PROMPT = "We're upgrading your login to client credentials for a simpler, more reliable experience and to support future security controls. Older Zapier SDK/CLI versions on this machine may stop working after the upgrade. Continue?";
14
+ const SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup` to generate a fresh signup URL and try again.";
15
+ const HEADLESS_SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup --headless` to generate a fresh signup URL and try again.";
16
+ function getEntryPointLabel(entryPoint) {
17
+ return entryPoint === "signup" ? "Signup" : "Login";
18
+ }
19
+ function getActiveCredentialsAction(entryPoint) {
20
+ return entryPoint === "signup" ? "continue signup" : "log in again";
21
+ }
22
+ function getCredentialsPromptMessage(entryPoint) {
23
+ return entryPoint === "signup"
24
+ ? "Enter a name to identify these credentials:"
25
+ : "Enter a name to identify them:";
26
+ }
27
+ function getProfileMessage(entryPoint, email) {
28
+ return entryPoint === "signup"
29
+ ? `👤 Authenticated as ${email}`
30
+ : `👤 Logged in as ${email}`;
31
+ }
32
+ function defaultCredentialsName(email) {
33
+ return `${email}@${hostname()}`;
34
+ }
35
+ function validateCredentialsName(name) {
36
+ const trimmedName = name.trim();
37
+ if (!trimmedName)
38
+ throw new ZapierCliValidationError("Name cannot be empty");
39
+ return trimmedName;
40
+ }
41
+ async function promptCredentialsName({ email, promptMessage, }) {
42
+ const { credentialName } = await inquirer.prompt([
43
+ {
44
+ type: "input",
45
+ name: "credentialName",
46
+ message: promptMessage,
47
+ default: defaultCredentialsName(email),
48
+ validate: (input) => {
49
+ try {
50
+ validateCredentialsName(input);
51
+ return true;
52
+ }
53
+ catch (err) {
54
+ return err instanceof Error ? err.message : String(err);
55
+ }
56
+ },
57
+ },
58
+ ]);
59
+ return validateCredentialsName(credentialName);
60
+ }
61
+ function resolveDefaultCredentialsName({ email, }) {
62
+ return validateCredentialsName(defaultCredentialsName(email));
63
+ }
64
+ function toPkceCredentials(credentials) {
65
+ if (credentials &&
66
+ isCredentialsObject(credentials) &&
67
+ !("clientSecret" in credentials)) {
68
+ return {
69
+ type: "pkce",
70
+ clientId: credentials.clientId,
71
+ baseUrl: credentials.baseUrl,
72
+ scope: credentials.scope,
73
+ };
74
+ }
75
+ return undefined;
76
+ }
77
+ function parseTimeoutSeconds(timeout) {
78
+ if (timeout === undefined)
79
+ return 300;
80
+ const timeoutSeconds = Number(timeout);
81
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
82
+ throw new ZapierCliValidationError("Timeout must be a positive integer (seconds).");
83
+ }
84
+ return timeoutSeconds;
85
+ }
86
+ async function promptConfirm({ message, defaultValue, }) {
87
+ const { confirmed } = await inquirer.prompt([
88
+ { type: "confirm", name: "confirmed", message, default: defaultValue },
89
+ ]);
90
+ return confirmed;
91
+ }
92
+ function promptlessCredentialResetError(credentials) {
93
+ throw new ZapierCliValidationError(`Already logged in as "${credentials.name}". Run \`logout\` first or use an interactive terminal to re-authenticate.`);
94
+ }
95
+ function promptlessLegacyJwtUpgradeError() {
96
+ throw new ZapierCliValidationError("Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials.");
97
+ }
98
+ async function clearExistingAuthState({ sdk, baseUrl, interactive, entryPoint, }) {
99
+ const activeCredentials = getActiveCredentials({ baseUrl });
100
+ const flowLabel = getEntryPointLabel(entryPoint);
101
+ if (activeCredentials) {
102
+ const confirmed = interactive
103
+ ? await promptConfirm({
104
+ defaultValue: false,
105
+ message: `You are already logged in as "${activeCredentials.name}".\n` +
106
+ "Logging out will delete these credentials and may interrupt other Zapier SDK or CLI sessions using them.\n" +
107
+ `Log out and ${getActiveCredentialsAction(entryPoint)}?`,
108
+ })
109
+ : promptlessCredentialResetError(activeCredentials);
110
+ if (!confirmed) {
111
+ console.log(`${flowLabel} cancelled.`);
112
+ return false;
113
+ }
114
+ try {
115
+ await revokeCredentials({
116
+ api: sdk.context.api,
117
+ credentials: activeCredentials,
118
+ });
119
+ }
120
+ catch {
121
+ if (!interactive) {
122
+ throw new ZapierCliValidationError(`${flowLabel} cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal.`);
123
+ }
124
+ const reset = await promptConfirm({
125
+ defaultValue: false,
126
+ message: `${flowLabel} cleanup failed. Reset local session state and continue?`,
127
+ });
128
+ if (!reset) {
129
+ console.log(`${flowLabel} cancelled.`);
130
+ return false;
131
+ }
132
+ await deleteStoredClientCredentials({
133
+ name: activeCredentials.name,
134
+ baseUrl: activeCredentials.baseUrl,
135
+ });
136
+ }
137
+ }
138
+ else if (hasLegacyJwtConfig()) {
139
+ const confirmed = interactive
140
+ ? await promptConfirm({
141
+ defaultValue: true,
142
+ message: LEGACY_JWT_UPGRADE_PROMPT,
143
+ })
144
+ : promptlessLegacyJwtUpgradeError();
145
+ if (!confirmed) {
146
+ console.log(`${flowLabel} cancelled.`);
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
152
+ async function getProfile(api) {
153
+ return api.get("/zapier/api/v4/profile/", {
154
+ authRequired: true,
155
+ });
156
+ }
157
+ async function saveClientCredentials({ api, name, credentialsBaseUrl, useApprovals, cleanupLogPrefix, }) {
158
+ await setupClientCredentials({
159
+ api,
160
+ name,
161
+ credentialsBaseUrl,
162
+ ...(useApprovals && { policy: EMPTY_POLICY }),
163
+ });
164
+ try {
165
+ await clearLegacyJwtState();
166
+ }
167
+ catch (err) {
168
+ console.error(`[${cleanupLogPrefix}] Best-effort legacy JWT cleanup failed:`, err);
169
+ }
170
+ }
171
+ function emitAccountAuthSuccess({ sdk, profile, }) {
172
+ sdk.context.eventEmission.emit("platform.sdk.ApplicationLifecycleEvent", buildApplicationLifecycleEvent({ lifecycle_event_type: "login_success" }, {
173
+ customuser_id: profile.user_id,
174
+ account_id: profile.roles[0]?.account_id ?? null,
175
+ }));
176
+ }
177
+ function emitSignupSuccess({ sdk, }) {
178
+ // `signup_success` tracks a valid signup OAuth callback being accepted.
179
+ // It does not mean a new Zapier account was created or local credentials were provisioned.
180
+ sdk.context.eventEmission.emit("platform.sdk.ApplicationLifecycleEvent", buildApplicationLifecycleEvent({ lifecycle_event_type: "signup_success" }));
181
+ }
182
+ async function runOauthWithRedaction(runOauth) {
183
+ try {
184
+ return await runOauth();
185
+ }
186
+ catch (error) {
187
+ if (error instanceof ZapierCliUserCancellationError)
188
+ throw error;
189
+ throw toRedactedOauthError(error);
190
+ }
191
+ }
192
+ async function runOauthForEntryPoint({ sdk, entryPoint, timeoutMs, pkceCredentials, baseUrl, headless, interactive, }) {
193
+ if (entryPoint === "signup") {
194
+ return runOauthWithRedaction(() => runSignupOauthFlow({
195
+ timeoutMs,
196
+ pkceCredentials,
197
+ baseUrl,
198
+ headless,
199
+ interactive,
200
+ recoveryMessage: headless
201
+ ? HEADLESS_SIGNUP_RECOVERY_MESSAGE
202
+ : SIGNUP_RECOVERY_MESSAGE,
203
+ onProgress: (event) => {
204
+ if (event.type === "callback_accepted") {
205
+ emitSignupSuccess({ sdk });
206
+ }
207
+ },
208
+ }));
209
+ }
210
+ return runOauthWithRedaction(() => runLoginOauthFlow({ timeoutMs, pkceCredentials, baseUrl }));
211
+ }
212
+ export async function runAccountAuth({ sdk, options, entryPoint, }) {
213
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
214
+ const interactive = !resolveNonInteractive(options);
215
+ const resolvedCredentials = await sdk.context.resolveCredentials();
216
+ const pkceCredentials = toPkceCredentials(resolvedCredentials);
217
+ const credentialsBaseUrl = await resolveCredentialsBaseUrl({
218
+ ...sdk.context,
219
+ resolvedCredentials,
220
+ });
221
+ if (!(await clearExistingAuthState({
222
+ sdk,
223
+ baseUrl: credentialsBaseUrl,
224
+ interactive,
225
+ entryPoint,
226
+ }))) {
227
+ return;
228
+ }
229
+ const { accessToken } = await runOauthForEntryPoint({
230
+ sdk,
231
+ entryPoint,
232
+ timeoutMs: timeoutSeconds * 1000,
233
+ pkceCredentials,
234
+ baseUrl: credentialsBaseUrl,
235
+ headless: options.headless === true,
236
+ interactive,
237
+ });
238
+ const scopedApi = getOrCreateApiClient({
239
+ credentials: accessToken,
240
+ baseUrl: credentialsBaseUrl,
241
+ });
242
+ const profile = await getProfile(scopedApi);
243
+ console.log(getProfileMessage(entryPoint, profile.email));
244
+ console.log("\nGenerating credentials so this machine can make authenticated requests on your behalf.");
245
+ const resolveCredentialsName = interactive
246
+ ? ({ email }) => promptCredentialsName({
247
+ email,
248
+ promptMessage: getCredentialsPromptMessage(entryPoint),
249
+ })
250
+ : resolveDefaultCredentialsName;
251
+ const credentialName = await resolveCredentialsName({ email: profile.email });
252
+ const useApprovals = options.useApprovals === true;
253
+ await saveClientCredentials({
254
+ api: scopedApi,
255
+ name: credentialName,
256
+ credentialsBaseUrl,
257
+ useApprovals,
258
+ cleanupLogPrefix: entryPoint,
259
+ });
260
+ console.log(`✅ Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`);
261
+ if (useApprovals) {
262
+ console.log("🔐 Approvals are enabled for these credentials.");
263
+ }
264
+ emitAccountAuthSuccess({ sdk, profile });
265
+ }
@@ -0,0 +1,6 @@
1
+ import type { OauthTransaction } from "./oauth-transaction";
2
+ export declare function getCallbackCode({ callbackUrl, transaction, recoveryMessage, }: {
3
+ callbackUrl: string;
4
+ transaction: Pick<OauthTransaction, "redirectUri" | "state">;
5
+ recoveryMessage?: string;
6
+ }): string;
@@ -0,0 +1,28 @@
1
+ import { ZapierCliValidationError } from "../errors";
2
+ export function getCallbackCode({ callbackUrl, transaction, recoveryMessage, }) {
3
+ let parsed;
4
+ try {
5
+ parsed = new URL(callbackUrl.trim());
6
+ }
7
+ catch {
8
+ throw new ZapierCliValidationError("Paste the final OAuth callback URL from your browser.");
9
+ }
10
+ const expected = new URL(transaction.redirectUri);
11
+ if (parsed.protocol !== "http:" ||
12
+ parsed.hostname !== expected.hostname ||
13
+ parsed.pathname !== expected.pathname ||
14
+ parsed.port !== expected.port) {
15
+ throw new ZapierCliValidationError(`Expected the final OAuth callback URL to start with ${transaction.redirectUri}.`);
16
+ }
17
+ if (parsed.searchParams.get("state") !== transaction.state) {
18
+ throw new ZapierCliValidationError(`OAuth state mismatch.${recoveryMessage ? ` ${recoveryMessage}` : ""}`);
19
+ }
20
+ if (parsed.searchParams.has("error")) {
21
+ throw new ZapierCliValidationError(`Authorization denied: ${parsed.searchParams.get("error_description") ?? parsed.searchParams.get("error")}.${recoveryMessage ? ` ${recoveryMessage}` : ""}`);
22
+ }
23
+ const code = parsed.searchParams.get("code");
24
+ if (!code) {
25
+ throw new ZapierCliValidationError("No authorization code found in the pasted callback URL.");
26
+ }
27
+ return code;
28
+ }
@@ -0,0 +1,2 @@
1
+ export declare function redactSensitiveOauthErrorMessage(message: string): string;
2
+ export declare function toRedactedOauthError(error: unknown): Error;
@@ -0,0 +1,39 @@
1
+ import { ZapierCliValidationError } from "../errors";
2
+ const SENSITIVE_OAUTH_FIELDS = [
3
+ "access_token",
4
+ "refresh_token",
5
+ "id_token",
6
+ "client_secret",
7
+ "code_verifier",
8
+ "code_challenge",
9
+ ];
10
+ function getErrorMessage(error) {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+ function toCamelCase(field) {
14
+ return field.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
15
+ }
16
+ function escapeRegExp(value) {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+ const sensitiveOauthFieldPattern = Array.from(new Set(SENSITIVE_OAUTH_FIELDS.flatMap((field) => [field, toCamelCase(field)])))
20
+ .map(escapeRegExp)
21
+ .join("|");
22
+ const sensitiveQueryParamPattern = new RegExp(`([?&])(${sensitiveOauthFieldPattern})(=)[^&#\\s"'<>]*`, "gi");
23
+ export function redactSensitiveOauthErrorMessage(message) {
24
+ return message
25
+ .replace(sensitiveQueryParamPattern, (_match, prefix, key, separator) => `${prefix}${key}${separator}[REDACTED]`)
26
+ .replace(new RegExp(`"(${sensitiveOauthFieldPattern})"(\\s*:\\s*)"[^"]*"`, "g"), (_match, key, separator) => `"${key}"${separator}"[REDACTED]"`);
27
+ }
28
+ export function toRedactedOauthError(error) {
29
+ const message = redactSensitiveOauthErrorMessage(getErrorMessage(error));
30
+ if (error instanceof ZapierCliValidationError) {
31
+ return new ZapierCliValidationError(message);
32
+ }
33
+ if (error instanceof Error) {
34
+ const redactedError = new Error(message);
35
+ redactedError.name = error.name;
36
+ return redactedError;
37
+ }
38
+ return new ZapierCliValidationError(message);
39
+ }
@@ -1,12 +1,37 @@
1
1
  import { type PkceCredentials } from "../../login";
2
- export interface OauthTokens {
3
- accessToken: string;
4
- refreshToken: string;
5
- expiresIn: number;
6
- }
2
+ import { buildBrowserAuthUrl, OAUTH_LOOPBACK_HOST, type OauthTokens } from "./oauth-transaction";
3
+ export { buildBrowserAuthUrl, OAUTH_LOOPBACK_HOST };
4
+ export type { OauthFlowEntryPoint, OauthTokens } from "./oauth-transaction";
5
+ export type OauthFlowProgressEvent = {
6
+ type: "browser_opening";
7
+ url: string;
8
+ } | {
9
+ type: "browser_opened";
10
+ url: string;
11
+ } | {
12
+ type: "browser_open_failed";
13
+ url: string;
14
+ reason: string;
15
+ } | {
16
+ type: "callback_waiting";
17
+ } | {
18
+ type: "callback_accepted";
19
+ } | {
20
+ type: "token_exchange_started";
21
+ } | {
22
+ type: "token_exchange_completed";
23
+ };
7
24
  export interface RunOauthFlowOptions {
8
25
  timeoutMs?: number;
9
26
  pkceCredentials?: PkceCredentials;
10
27
  baseUrl?: string;
28
+ silent?: boolean;
29
+ onProgress?: (event: OauthFlowProgressEvent) => void;
30
+ recoveryMessage?: string;
31
+ }
32
+ interface RunSignupOauthFlowOptions extends RunOauthFlowOptions {
33
+ headless?: boolean;
34
+ interactive?: boolean;
11
35
  }
12
- export declare function runOauthFlow({ timeoutMs, pkceCredentials, baseUrl, }: RunOauthFlowOptions): Promise<OauthTokens>;
36
+ export declare function runLoginOauthFlow(options: RunOauthFlowOptions): Promise<OauthTokens>;
37
+ export declare function runSignupOauthFlow(options: RunSignupOauthFlowOptions): Promise<OauthTokens>;