@zapier/zapier-sdk-cli 0.52.12 → 0.53.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 (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +20 -0
  3. package/dist/cli.cjs +896 -395
  4. package/dist/cli.mjs +897 -396
  5. package/dist/experimental.cjs +897 -398
  6. package/dist/experimental.d.mts +1 -1
  7. package/dist/experimental.d.ts +1 -1
  8. package/dist/experimental.mjs +896 -397
  9. package/dist/index.cjs +898 -399
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.mjs +897 -398
  13. package/dist/package.json +1 -1
  14. package/dist/{sdk-Sa1HjzUj.d.mts → sdk-SOLizjno.d.mts} +40 -2
  15. package/dist/{sdk-Sa1HjzUj.d.ts → sdk-SOLizjno.d.ts} +40 -2
  16. package/dist/src/experimental.js +2 -1
  17. package/dist/src/plugins/index.d.ts +1 -0
  18. package/dist/src/plugins/index.js +1 -0
  19. package/dist/src/plugins/login/index.d.ts +2 -15
  20. package/dist/src/plugins/login/index.js +3 -191
  21. package/dist/src/plugins/signup/index.d.ts +25 -0
  22. package/dist/src/plugins/signup/index.js +12 -0
  23. package/dist/src/plugins/signup/schemas.d.ts +9 -0
  24. package/dist/src/plugins/signup/schemas.js +26 -0
  25. package/dist/src/plugins/signup/test-harness.d.ts +34 -0
  26. package/dist/src/plugins/signup/test-harness.js +74 -0
  27. package/dist/src/sdk.js +2 -1
  28. package/dist/src/types/sdk.d.ts +2 -1
  29. package/dist/src/utils/auth/account-auth.d.ts +32 -0
  30. package/dist/src/utils/auth/account-auth.js +265 -0
  31. package/dist/src/utils/auth/oauth-callback.d.ts +6 -0
  32. package/dist/src/utils/auth/oauth-callback.js +28 -0
  33. package/dist/src/utils/auth/oauth-errors.d.ts +2 -0
  34. package/dist/src/utils/auth/oauth-errors.js +39 -0
  35. package/dist/src/utils/auth/oauth-flow.d.ts +31 -6
  36. package/dist/src/utils/auth/oauth-flow.js +258 -106
  37. package/dist/src/utils/auth/oauth-transaction.d.ts +35 -0
  38. package/dist/src/utils/auth/oauth-transaction.js +69 -0
  39. package/dist/src/utils/non-interactive.d.ts +5 -4
  40. package/dist/src/utils/non-interactive.js +6 -5
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +3 -3
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, CommanderError, Option } from 'commander';
3
3
  import { z } from 'zod';
4
- import { definePlugin, createPluginMethod, getOrCreateApiClient, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, BaseSdkOptionsSchema, isCredentialsObject, invalidateCachedToken, buildApplicationLifecycleEvent, batch, toSnakeCase, ZapierAbortDrainSignal, createZapierSdkStack as createZapierSdkStack$1, addPlugin as addPlugin$1, ZapierError, ZapierAuthenticationError, isPositional, runWithTelemetryContext, buildCapabilityMessage, formatErrorMessage, getOsInfo, getPlatformVersions, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId } from '@zapier/zapier-sdk';
4
+ import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, BaseSdkOptionsSchema, getOrCreateApiClient, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, createZapierSdkStack as createZapierSdkStack$1, addPlugin as addPlugin$1, ZapierError, isCredentialsObject, buildApplicationLifecycleEvent, ZapierAuthenticationError, isPositional, runWithTelemetryContext, buildCapabilityMessage, formatErrorMessage, getOsInfo, getPlatformVersions, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId } from '@zapier/zapier-sdk';
5
5
  import inquirer from 'inquirer';
6
6
  import search from '@inquirer/search';
7
7
  import chalk from 'chalk';
@@ -9,7 +9,7 @@ import ora from 'ora';
9
9
  import util from 'util';
10
10
  import wrapAnsi from 'wrap-ansi';
11
11
  import * as jwt from 'jsonwebtoken';
12
- import { deletePassword, getKeyring, getPassword, setPassword } from 'cross-keychain';
12
+ import { deletePassword, setPassword, getKeyring, getPassword } from 'cross-keychain';
13
13
  import Conf from 'conf';
14
14
  import * as fs from 'fs';
15
15
  import { promises, createWriteStream, existsSync, readdirSync, rmSync, mkdirSync, writeFileSync, copyFileSync, readFileSync } from 'fs';
@@ -18,8 +18,9 @@ import * as path from 'path';
18
18
  import { resolve, join, dirname, basename, relative, extname } from 'path';
19
19
  import * as lockfile from 'proper-lockfile';
20
20
  import { hostname } from 'os';
21
- import open from 'open';
22
21
  import express from 'express';
22
+ import { createInterface } from 'readline/promises';
23
+ import open from 'open';
23
24
  import pkceChallenge from 'pkce-challenge';
24
25
  import { startMcpServer } from '@zapier/zapier-sdk-mcp';
25
26
  import { buildSync } from 'esbuild';
@@ -1530,7 +1531,7 @@ var SHARED_COMMAND_CLI_OPTIONS = [
1530
1531
 
1531
1532
  // package.json
1532
1533
  var package_default = {
1533
- version: "0.52.12"};
1534
+ version: "0.53.0"};
1534
1535
 
1535
1536
  // src/telemetry/builders.ts
1536
1537
  function createCliBaseEvent(context = {}) {
@@ -3502,25 +3503,112 @@ async function revokeCredentials({
3502
3503
  });
3503
3504
  emitAuthLogout(onEvent);
3504
3505
  }
3506
+ function getBaseUrlFromResolvedCredentials(credentials2) {
3507
+ if (credentials2 && isCredentialsObject(credentials2)) {
3508
+ return credentials2.baseUrl;
3509
+ }
3510
+ return void 0;
3511
+ }
3512
+ function getBaseUrlFromOptionsCredentials(credentials2) {
3513
+ if (credentials2 && typeof credentials2 === "object" && "baseUrl" in credentials2 && typeof credentials2.baseUrl === "string") {
3514
+ return credentials2.baseUrl;
3515
+ }
3516
+ return void 0;
3517
+ }
3518
+ async function resolveCredentialsBaseUrl(context) {
3519
+ const resolvedCredentials = "resolvedCredentials" in context ? context.resolvedCredentials : await context.resolveCredentials?.();
3520
+ return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
3521
+ }
3505
3522
 
3506
- // src/utils/constants.ts
3507
- var LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
3508
- var LOGIN_TIMEOUT_MS = 3e5;
3509
- var spinPromise = async (promise, text) => {
3510
- const spinner = ora(text).start();
3523
+ // src/utils/non-interactive.ts
3524
+ function resolveNonInteractive(options) {
3525
+ return options.nonInteractive === true || options.skipPrompts === true || !process.stdin.isTTY || !process.stdout.isTTY;
3526
+ }
3527
+
3528
+ // src/utils/auth/client-credentials.ts
3529
+ var CREDENTIALS_SCOPES = ["external", "credentials"];
3530
+ var EMPTY_POLICY = {
3531
+ version: 2,
3532
+ statements: []
3533
+ };
3534
+ async function createCredentialsOnServer(api2, name, policy) {
3535
+ const response = await api2.post(
3536
+ "/api/v0/client-credentials",
3537
+ {
3538
+ name,
3539
+ allowed_scopes: CREDENTIALS_SCOPES,
3540
+ ...policy !== void 0 && { policy }
3541
+ },
3542
+ { authRequired: true, requiredScopes: ["credentials"] }
3543
+ );
3544
+ return {
3545
+ clientId: response.data.client_id,
3546
+ clientSecret: response.data.client_secret
3547
+ };
3548
+ }
3549
+ async function deleteCredentialsOnServer(api2, clientId) {
3550
+ await api2.delete(`/api/v0/client-credentials/${clientId}`, void 0, {
3551
+ authRequired: true,
3552
+ requiredScopes: ["credentials"]
3553
+ });
3554
+ }
3555
+ async function setupClientCredentials({
3556
+ api: api2,
3557
+ name,
3558
+ credentialsBaseUrl: credentialsBaseUrl2,
3559
+ policy
3560
+ }) {
3561
+ const { clientId, clientSecret } = await createCredentialsOnServer(
3562
+ api2,
3563
+ name,
3564
+ policy
3565
+ );
3511
3566
  try {
3512
- const result = await promise;
3513
- spinner.succeed();
3514
- return result;
3515
- } catch (error) {
3516
- if (error instanceof ZapierCliUserCancellationError) {
3517
- spinner.stop();
3518
- } else {
3519
- spinner.fail();
3567
+ await withRetry({
3568
+ action: () => storeClientCredentials({
3569
+ name,
3570
+ clientId,
3571
+ clientSecret,
3572
+ scopes: [...CREDENTIALS_SCOPES],
3573
+ baseUrl: credentialsBaseUrl2
3574
+ })
3575
+ });
3576
+ } catch (storeErr) {
3577
+ try {
3578
+ await withRetry({
3579
+ action: () => deleteCredentialsOnServer(api2, clientId)
3580
+ });
3581
+ } catch {
3582
+ console.error(
3583
+ `Failed to roll back orphaned credential ${clientId}. Delete it manually with: zapier-sdk delete-client-credentials ${clientId}`
3584
+ );
3520
3585
  }
3521
- throw error;
3586
+ throw storeErr;
3522
3587
  }
3588
+ return { clientId };
3589
+ }
3590
+
3591
+ // src/utils/constants.ts
3592
+ var LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
3593
+ var LOGIN_TIMEOUT_MS = 3e5;
3594
+
3595
+ // src/utils/getCallablePromise.ts
3596
+ var getCallablePromise = () => {
3597
+ let resolve4 = () => {
3598
+ };
3599
+ let reject = () => {
3600
+ };
3601
+ const promise = new Promise((_resolve, _reject) => {
3602
+ resolve4 = _resolve;
3603
+ reject = _reject;
3604
+ });
3605
+ return {
3606
+ promise,
3607
+ resolve: resolve4,
3608
+ reject
3609
+ };
3523
3610
  };
3611
+ var getCallablePromise_default = getCallablePromise;
3524
3612
  var log = {
3525
3613
  info: (message, ...args) => {
3526
3614
  console.error(chalk.blue("\u2139"), message, ...args);
@@ -3541,6 +3629,60 @@ var log = {
3541
3629
  }
3542
3630
  };
3543
3631
  var log_default = log;
3632
+ var spinPromise = async (promise, text) => {
3633
+ const spinner = ora(text).start();
3634
+ try {
3635
+ const result = await promise;
3636
+ spinner.succeed();
3637
+ return result;
3638
+ } catch (error) {
3639
+ if (error instanceof ZapierCliUserCancellationError) {
3640
+ spinner.stop();
3641
+ } else {
3642
+ spinner.fail();
3643
+ }
3644
+ throw error;
3645
+ }
3646
+ };
3647
+
3648
+ // src/utils/auth/oauth-callback.ts
3649
+ function getCallbackCode({
3650
+ callbackUrl,
3651
+ transaction,
3652
+ recoveryMessage
3653
+ }) {
3654
+ let parsed;
3655
+ try {
3656
+ parsed = new URL(callbackUrl.trim());
3657
+ } catch {
3658
+ throw new ZapierCliValidationError(
3659
+ "Paste the final OAuth callback URL from your browser."
3660
+ );
3661
+ }
3662
+ const expected = new URL(transaction.redirectUri);
3663
+ if (parsed.protocol !== "http:" || parsed.hostname !== expected.hostname || parsed.pathname !== expected.pathname || parsed.port !== expected.port) {
3664
+ throw new ZapierCliValidationError(
3665
+ `Expected the final OAuth callback URL to start with ${transaction.redirectUri}.`
3666
+ );
3667
+ }
3668
+ if (parsed.searchParams.get("state") !== transaction.state) {
3669
+ throw new ZapierCliValidationError(
3670
+ `OAuth state mismatch.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
3671
+ );
3672
+ }
3673
+ if (parsed.searchParams.has("error")) {
3674
+ throw new ZapierCliValidationError(
3675
+ `Authorization denied: ${parsed.searchParams.get("error_description") ?? parsed.searchParams.get("error")}.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
3676
+ );
3677
+ }
3678
+ const code = parsed.searchParams.get("code");
3679
+ if (!code) {
3680
+ throw new ZapierCliValidationError(
3681
+ "No authorization code found in the pasted callback URL."
3682
+ );
3683
+ }
3684
+ return code;
3685
+ }
3544
3686
 
3545
3687
  // src/utils/api/client.ts
3546
3688
  var createApiClient = () => {
@@ -3571,179 +3713,77 @@ var createApiClient = () => {
3571
3713
  var api = createApiClient();
3572
3714
  var client_default = api;
3573
3715
 
3574
- // src/utils/getCallablePromise.ts
3575
- var getCallablePromise = () => {
3576
- let resolve4 = () => {
3577
- };
3578
- let reject = () => {
3579
- };
3580
- const promise = new Promise((_resolve, _reject) => {
3581
- resolve4 = _resolve;
3582
- reject = _reject;
3583
- });
3584
- return {
3585
- promise,
3586
- resolve: resolve4,
3587
- reject
3588
- };
3589
- };
3590
- var getCallablePromise_default = getCallablePromise;
3591
-
3592
- // src/utils/auth/oauth-flow.ts
3593
- var findAvailablePort = () => {
3594
- return new Promise((resolve4, reject) => {
3595
- let portIndex = 0;
3596
- const tryPort = (port) => {
3597
- const server = express().listen(port, () => {
3598
- server.close();
3599
- resolve4(port);
3600
- });
3601
- server.on("error", (err) => {
3602
- if (err.code === "EADDRINUSE") {
3603
- if (portIndex < LOGIN_PORTS.length) {
3604
- tryPort(LOGIN_PORTS[portIndex++]);
3605
- } else {
3606
- reject(
3607
- new Error(
3608
- `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
3609
- )
3610
- );
3611
- }
3612
- } else {
3613
- reject(err);
3614
- }
3615
- });
3616
- };
3617
- if (LOGIN_PORTS.length > 0) {
3618
- tryPort(LOGIN_PORTS[portIndex++]);
3619
- } else {
3620
- reject(new Error("No OAuth callback ports configured"));
3621
- }
3622
- });
3623
- };
3624
- var generateRandomString = () => {
3716
+ // src/utils/auth/oauth-transaction.ts
3717
+ var OAUTH_LOOPBACK_HOST = "localhost";
3718
+ function buildBrowserAuthUrl({
3719
+ authorizeUrl,
3720
+ entryPoint = "login"
3721
+ }) {
3722
+ if (entryPoint === "login") return authorizeUrl;
3723
+ const parsedAuthorizeUrl = new URL(authorizeUrl);
3724
+ const signupUrl = new URL("/sign-up", parsedAuthorizeUrl);
3725
+ signupUrl.searchParams.set("skipOnboarding", "true");
3726
+ signupUrl.searchParams.set(
3727
+ "next",
3728
+ `${parsedAuthorizeUrl.pathname}${parsedAuthorizeUrl.search}`
3729
+ );
3730
+ return signupUrl.toString();
3731
+ }
3732
+ function generateRandomString() {
3625
3733
  const array = new Uint32Array(28);
3626
3734
  crypto.getRandomValues(array);
3627
3735
  return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join(
3628
3736
  ""
3629
3737
  );
3630
- };
3738
+ }
3631
3739
  function ensureOfflineAccess(scope) {
3632
- if (scope.includes("offline_access")) {
3633
- return scope;
3634
- }
3740
+ if (scope.includes("offline_access")) return scope;
3635
3741
  return `${scope} offline_access`;
3636
3742
  }
3637
- async function runOauthFlow({
3638
- timeoutMs = LOGIN_TIMEOUT_MS,
3743
+ async function prepareOauthTransaction({
3639
3744
  pkceCredentials,
3640
- baseUrl: baseUrl2
3745
+ baseUrl: baseUrl2,
3746
+ redirectUri,
3747
+ entryPoint = "login"
3641
3748
  }) {
3642
3749
  const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
3643
3750
  credentials: pkceCredentials,
3644
3751
  baseUrl: baseUrl2
3645
3752
  });
3646
- const scope = ensureOfflineAccess(
3647
- pkceCredentials?.scope || "internal credentials"
3648
- );
3649
- const availablePort = await findAvailablePort();
3650
- const redirectUri = `http://localhost:${availablePort}/oauth`;
3651
- log_default.info(`Using port ${availablePort} for OAuth callback`);
3652
- const {
3653
- promise: promisedCode,
3654
- resolve: setCode,
3655
- reject: rejectCode
3656
- } = getCallablePromise_default();
3657
- const oauthState = generateRandomString();
3658
- const expressApp = express();
3659
- expressApp.get("/oauth", (req, res) => {
3660
- res.setHeader("Connection", "close");
3661
- if (req.query.state !== oauthState) {
3662
- rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF"));
3663
- res.status(400).end("Invalid state. You can close this tab.");
3664
- return;
3665
- }
3666
- if (req.query.error) {
3667
- const desc = req.query.error_description ?? req.query.error;
3668
- rejectCode(new Error(`Authorization denied: ${desc}`));
3669
- res.end("Authorization was denied. You can close this tab.");
3670
- return;
3671
- }
3672
- if (!req.query.code) {
3673
- rejectCode(new Error("No authorization code received"));
3674
- res.end("No authorization code received. You can close this tab.");
3675
- return;
3676
- }
3677
- setCode(String(req.query.code));
3678
- res.end("You can now close this tab and return to the CLI.");
3679
- });
3680
- const server = expressApp.listen(availablePort);
3681
- const connections = /* @__PURE__ */ new Set();
3682
- server.on("connection", (conn) => {
3683
- connections.add(conn);
3684
- conn.on("close", () => connections.delete(conn));
3685
- });
3686
- const cleanup = () => {
3687
- server.close();
3688
- log_default.info("\n\u274C Login cancelled by user");
3689
- rejectCode(new ZapierCliUserCancellationError());
3690
- };
3691
- process.on("SIGINT", cleanup);
3692
- process.on("SIGTERM", cleanup);
3693
3753
  const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
3754
+ const state = generateRandomString();
3694
3755
  const authUrl = `${authorizeUrl}?${new URLSearchParams({
3695
3756
  response_type: "code",
3696
3757
  client_id: clientId,
3697
3758
  redirect_uri: redirectUri,
3698
- scope,
3699
- state: oauthState,
3759
+ scope: ensureOfflineAccess(
3760
+ pkceCredentials?.scope || "internal credentials"
3761
+ ),
3762
+ state,
3700
3763
  code_challenge: codeChallenge,
3701
3764
  code_challenge_method: "S256"
3702
3765
  }).toString()}`;
3703
- log_default.info("Opening your browser to log in.");
3704
- log_default.info("If it doesn't open, visit:", authUrl);
3705
- open(authUrl);
3706
- let timeoutTimer;
3707
- try {
3708
- await spinPromise(
3709
- Promise.race([
3710
- promisedCode,
3711
- new Promise((_resolve, reject) => {
3712
- timeoutTimer = setTimeout(() => {
3713
- reject(
3714
- new Error(
3715
- `Login timed out after ${Math.round(timeoutMs / 1e3)} seconds.`
3716
- )
3717
- );
3718
- }, timeoutMs);
3719
- })
3720
- ]),
3721
- "Waiting for you to login and authorize"
3722
- );
3723
- } finally {
3724
- if (timeoutTimer) {
3725
- clearTimeout(timeoutTimer);
3726
- }
3727
- process.off("SIGINT", cleanup);
3728
- process.off("SIGTERM", cleanup);
3729
- await new Promise((resolve4) => {
3730
- const timeout = setTimeout(() => {
3731
- log_default.info("Server close timed out, forcing connection shutdown...");
3732
- connections.forEach((conn) => conn.destroy());
3733
- resolve4();
3734
- }, 1e3);
3735
- server.close(() => {
3736
- clearTimeout(timeout);
3737
- resolve4();
3738
- });
3739
- });
3740
- }
3741
- log_default.info("Exchanging authorization code for tokens...");
3766
+ return {
3767
+ browserAuthUrl: buildBrowserAuthUrl({ authorizeUrl: authUrl, entryPoint }),
3768
+ clientId,
3769
+ codeVerifier,
3770
+ redirectUri,
3771
+ state,
3772
+ tokenUrl
3773
+ };
3774
+ }
3775
+ async function exchangeOauthCode({
3776
+ tokenUrl,
3777
+ code,
3778
+ redirectUri,
3779
+ clientId,
3780
+ codeVerifier
3781
+ }) {
3742
3782
  const { data } = await client_default.post(
3743
3783
  tokenUrl,
3744
3784
  {
3745
3785
  grant_type: "authorization_code",
3746
- code: await promisedCode,
3786
+ code,
3747
3787
  redirect_uri: redirectUri,
3748
3788
  client_id: clientId,
3749
3789
  code_verifier: codeVerifier
@@ -3755,7 +3795,6 @@ async function runOauthFlow({
3755
3795
  }
3756
3796
  }
3757
3797
  );
3758
- log_default.info("Token exchange completed successfully");
3759
3798
  return {
3760
3799
  accessToken: data.access_token,
3761
3800
  refreshToken: data.refresh_token,
@@ -3763,105 +3802,467 @@ async function runOauthFlow({
3763
3802
  };
3764
3803
  }
3765
3804
 
3766
- // src/utils/auth/client-credentials.ts
3767
- var CREDENTIALS_SCOPES = ["external", "credentials"];
3768
- var EMPTY_POLICY = {
3769
- version: 2,
3770
- statements: []
3805
+ // src/utils/auth/oauth-flow.ts
3806
+ var OauthFlowTimeoutError = class extends Error {
3807
+ constructor(timeoutMs) {
3808
+ super("OAuth flow timed out");
3809
+ this.timeoutMs = timeoutMs;
3810
+ this.name = "OauthFlowTimeoutError";
3811
+ }
3771
3812
  };
3772
- async function createCredentialsOnServer(api2, name, policy) {
3773
- const response = await api2.post(
3774
- "/api/v0/client-credentials",
3775
- {
3776
- name,
3777
- allowed_scopes: CREDENTIALS_SCOPES,
3778
- ...policy !== void 0 && { policy }
3779
- },
3780
- { authRequired: true, requiredScopes: ["credentials"] }
3781
- );
3782
- return {
3783
- clientId: response.data.client_id,
3784
- clientSecret: response.data.client_secret
3785
- };
3813
+ var OauthAuthorizationDeniedError = class extends Error {
3814
+ constructor(reason) {
3815
+ super("OAuth authorization denied");
3816
+ this.reason = reason;
3817
+ this.name = "OauthAuthorizationDeniedError";
3818
+ }
3819
+ };
3820
+ function findAvailablePort() {
3821
+ return new Promise((resolve4, reject) => {
3822
+ let portIndex = 0;
3823
+ const tryPort = (port) => {
3824
+ const server = express().listen(port, OAUTH_LOOPBACK_HOST, () => {
3825
+ server.close();
3826
+ resolve4(port);
3827
+ });
3828
+ server.on("error", (err) => {
3829
+ if (err.code === "EADDRINUSE" && portIndex < LOGIN_PORTS.length) {
3830
+ tryPort(LOGIN_PORTS[portIndex++]);
3831
+ } else if (err.code === "EADDRINUSE") {
3832
+ reject(
3833
+ new Error(
3834
+ `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
3835
+ )
3836
+ );
3837
+ } else {
3838
+ reject(err);
3839
+ }
3840
+ });
3841
+ };
3842
+ if (LOGIN_PORTS.length > 0) tryPort(LOGIN_PORTS[portIndex++]);
3843
+ else reject(new Error("No OAuth callback ports configured"));
3844
+ });
3786
3845
  }
3787
- async function deleteCredentialsOnServer(api2, clientId) {
3788
- await api2.delete(`/api/v0/client-credentials/${clientId}`, void 0, {
3789
- authRequired: true,
3790
- requiredScopes: ["credentials"]
3846
+ async function runLoginOauthFlow(options) {
3847
+ return runOauthFlowEntryPoint({
3848
+ ...options,
3849
+ entryPoint: "login",
3850
+ authAction: "log in",
3851
+ flowName: "Login"
3791
3852
  });
3792
3853
  }
3793
- async function setupClientCredentials({
3794
- api: api2,
3795
- name,
3796
- credentialsBaseUrl: credentialsBaseUrl2,
3797
- policy
3854
+ async function runSignupOauthFlow(options) {
3855
+ if (options.headless) {
3856
+ return runOauthFlowEntryPoint({
3857
+ ...options,
3858
+ entryPoint: "signup",
3859
+ authAction: "sign up",
3860
+ flowName: "Signup",
3861
+ headless: true
3862
+ });
3863
+ }
3864
+ return runOauthFlowEntryPoint({
3865
+ ...options,
3866
+ entryPoint: "signup",
3867
+ authAction: "sign up",
3868
+ flowName: "Signup"
3869
+ });
3870
+ }
3871
+ async function runOauthFlowEntryPoint({
3872
+ flowName,
3873
+ ...options
3798
3874
  }) {
3799
- const { clientId, clientSecret } = await createCredentialsOnServer(
3800
- api2,
3801
- name,
3802
- policy
3875
+ try {
3876
+ return options.headless ? await runHeadlessSignupOauthFlow(options) : await runOauthFlow(options);
3877
+ } catch (error) {
3878
+ if (error instanceof OauthFlowTimeoutError) {
3879
+ throw new Error(
3880
+ withRecoveryMessage(
3881
+ `${flowName} timed out after ${Math.round(error.timeoutMs / 1e3)} seconds.`,
3882
+ options.recoveryMessage
3883
+ )
3884
+ );
3885
+ }
3886
+ if (error instanceof OauthAuthorizationDeniedError) {
3887
+ throw new Error(
3888
+ withRecoveryMessage(
3889
+ `Authorization denied: ${error.reason}.`,
3890
+ options.recoveryMessage
3891
+ )
3892
+ );
3893
+ }
3894
+ if (error instanceof ZapierCliUserCancellationError && !options.silent) {
3895
+ log_default.info(`
3896
+ \u274C ${flowName} cancelled by user`);
3897
+ }
3898
+ throw error;
3899
+ }
3900
+ }
3901
+ function withRecoveryMessage(message, recoveryMessage) {
3902
+ return recoveryMessage ? `${message} ${recoveryMessage}` : message;
3903
+ }
3904
+ async function runOauthFlow({
3905
+ timeoutMs = LOGIN_TIMEOUT_MS,
3906
+ pkceCredentials,
3907
+ baseUrl: baseUrl2,
3908
+ entryPoint,
3909
+ authAction,
3910
+ silent = false,
3911
+ onProgress
3912
+ }) {
3913
+ const port = await findAvailablePort();
3914
+ if (!silent) log_default.info(`Using port ${port} for OAuth callback`);
3915
+ const transaction = await prepareOauthTransaction({
3916
+ pkceCredentials,
3917
+ baseUrl: baseUrl2,
3918
+ redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
3919
+ entryPoint
3920
+ });
3921
+ const code = await collectLocalCallbackCode({
3922
+ transaction,
3923
+ timeoutMs,
3924
+ authAction,
3925
+ silent,
3926
+ onProgress
3927
+ });
3928
+ onProgress?.({ type: "callback_accepted" });
3929
+ if (!silent) log_default.info("Exchanging authorization code for tokens...");
3930
+ onProgress?.({ type: "token_exchange_started" });
3931
+ const tokens = await exchangeOauthCode({ ...transaction, code });
3932
+ if (!silent) log_default.info("Token exchange completed successfully");
3933
+ onProgress?.({ type: "token_exchange_completed" });
3934
+ return tokens;
3935
+ }
3936
+ async function readHeadlessCallbackUrl({
3937
+ timeoutMs,
3938
+ interactive,
3939
+ recoveryMessage
3940
+ }) {
3941
+ const timeoutMessage = withRecoveryMessage(
3942
+ `Signup timed out after ${Math.round(timeoutMs / 1e3)} seconds.`,
3943
+ recoveryMessage
3944
+ );
3945
+ const missingCallbackUrlMessage = withRecoveryMessage(
3946
+ "Paste the final OAuth callback URL from your browser.",
3947
+ recoveryMessage
3803
3948
  );
3949
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
3950
+ const abortController = new AbortController();
3951
+ const timeoutTimer = setTimeout(() => abortController.abort(), timeoutMs);
3952
+ const readUrl = interactive ? rl.question("Paste the final OAuth callback URL: ", {
3953
+ signal: abortController.signal
3954
+ }) : new Promise((resolve4, reject) => {
3955
+ let settled = false;
3956
+ const settleResolve = (value) => {
3957
+ settled = true;
3958
+ resolve4(value);
3959
+ };
3960
+ const settleReject = (error) => {
3961
+ if (settled) return;
3962
+ settled = true;
3963
+ reject(error);
3964
+ };
3965
+ abortController.signal.addEventListener(
3966
+ "abort",
3967
+ () => settleReject(new Error(timeoutMessage)),
3968
+ { once: true }
3969
+ );
3970
+ rl.once("line", settleResolve);
3971
+ rl.once(
3972
+ "close",
3973
+ () => settleReject(new ZapierCliValidationError(missingCallbackUrlMessage))
3974
+ );
3975
+ rl.once("error", settleReject);
3976
+ });
3804
3977
  try {
3805
- await withRetry({
3806
- action: () => storeClientCredentials({
3807
- name,
3808
- clientId,
3809
- clientSecret,
3810
- scopes: [...CREDENTIALS_SCOPES],
3811
- baseUrl: credentialsBaseUrl2
3812
- })
3978
+ return await readUrl.catch((error) => {
3979
+ if (error instanceof Error && error.name === "AbortError") {
3980
+ throw new Error(timeoutMessage);
3981
+ }
3982
+ throw error;
3813
3983
  });
3814
- } catch (storeErr) {
3815
- try {
3816
- await withRetry({
3817
- action: () => deleteCredentialsOnServer(api2, clientId)
3818
- });
3819
- } catch {
3820
- console.error(
3821
- `Failed to roll back orphaned credential ${clientId}. Delete it manually with: zapier-sdk delete-client-credentials ${clientId}`
3984
+ } finally {
3985
+ clearTimeout(timeoutTimer);
3986
+ rl.close();
3987
+ }
3988
+ }
3989
+ async function runHeadlessSignupOauthFlow({
3990
+ timeoutMs = LOGIN_TIMEOUT_MS,
3991
+ pkceCredentials,
3992
+ baseUrl: baseUrl2,
3993
+ interactive = true,
3994
+ onProgress,
3995
+ recoveryMessage
3996
+ }) {
3997
+ const port = LOGIN_PORTS[0];
3998
+ const transaction = await prepareOauthTransaction({
3999
+ pkceCredentials,
4000
+ baseUrl: baseUrl2,
4001
+ redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
4002
+ entryPoint: "signup"
4003
+ });
4004
+ console.log(
4005
+ "Use this mode when signing up from a machine that has no browser."
4006
+ );
4007
+ console.log("Open this signup URL in a browser on another machine:");
4008
+ console.log(transaction.browserAuthUrl);
4009
+ console.log(
4010
+ `When the browser lands on ${transaction.redirectUri} and cannot connect, paste the full final URL back here.`
4011
+ );
4012
+ const callbackUrl = await readHeadlessCallbackUrl({
4013
+ timeoutMs,
4014
+ interactive,
4015
+ recoveryMessage
4016
+ });
4017
+ const code = getCallbackCode({
4018
+ callbackUrl,
4019
+ transaction,
4020
+ recoveryMessage
4021
+ });
4022
+ onProgress?.({ type: "callback_accepted" });
4023
+ console.log("Exchanging authorization code for tokens...");
4024
+ onProgress?.({ type: "token_exchange_started" });
4025
+ const tokens = await exchangeOauthCode({ ...transaction, code });
4026
+ onProgress?.({ type: "token_exchange_completed" });
4027
+ return tokens;
4028
+ }
4029
+ async function collectLocalCallbackCode({
4030
+ transaction,
4031
+ timeoutMs,
4032
+ authAction,
4033
+ silent,
4034
+ onProgress
4035
+ }) {
4036
+ const { promise, resolve: resolve4, reject } = getCallablePromise_default();
4037
+ const app = express();
4038
+ app.get("/oauth", (req, res) => {
4039
+ res.setHeader("Connection", "close");
4040
+ if (req.query.state !== transaction.state) {
4041
+ res.status(400).end("Invalid state. You can close this tab.");
4042
+ } else if (req.query.error) {
4043
+ reject(
4044
+ new OauthAuthorizationDeniedError(
4045
+ String(req.query.error_description ?? req.query.error)
4046
+ )
3822
4047
  );
4048
+ res.end("Authorization was denied. You can close this tab.");
4049
+ } else if (!req.query.code) {
4050
+ reject(new Error("No authorization code received"));
4051
+ res.end("No authorization code received. You can close this tab.");
4052
+ } else {
4053
+ resolve4(String(req.query.code));
4054
+ res.end("You can now close this tab and return to the CLI.");
3823
4055
  }
3824
- throw storeErr;
4056
+ });
4057
+ const server = app.listen(
4058
+ Number(new URL(transaction.redirectUri).port),
4059
+ OAUTH_LOOPBACK_HOST
4060
+ );
4061
+ const connections = /* @__PURE__ */ new Set();
4062
+ server.on("connection", (conn) => {
4063
+ connections.add(conn);
4064
+ conn.on("close", () => connections.delete(conn));
4065
+ });
4066
+ const cleanup = () => {
4067
+ server.close();
4068
+ reject(new ZapierCliUserCancellationError());
4069
+ };
4070
+ process.on("SIGINT", cleanup);
4071
+ process.on("SIGTERM", cleanup);
4072
+ let timeoutTimer;
4073
+ try {
4074
+ await waitForServerListening(server);
4075
+ await openBrowser({ transaction, authAction, silent, onProgress });
4076
+ const waitForCode = Promise.race([
4077
+ promise,
4078
+ new Promise((_resolve, rejectTimeout) => {
4079
+ timeoutTimer = setTimeout(() => {
4080
+ rejectTimeout(new OauthFlowTimeoutError(timeoutMs));
4081
+ }, timeoutMs);
4082
+ })
4083
+ ]);
4084
+ onProgress?.({ type: "callback_waiting" });
4085
+ return silent ? await waitForCode : await spinPromise(
4086
+ waitForCode,
4087
+ `Waiting for you to ${authAction} and authorize`
4088
+ );
4089
+ } finally {
4090
+ if (timeoutTimer) clearTimeout(timeoutTimer);
4091
+ process.off("SIGINT", cleanup);
4092
+ process.off("SIGTERM", cleanup);
4093
+ await closeServer({ server, connections, silent });
3825
4094
  }
3826
- return { clientId };
3827
4095
  }
3828
- function getBaseUrlFromResolvedCredentials(credentials2) {
3829
- if (credentials2 && isCredentialsObject(credentials2)) {
3830
- return credentials2.baseUrl;
3831
- }
3832
- return void 0;
4096
+ async function waitForServerListening(server) {
4097
+ if (server.listening) return;
4098
+ await new Promise((resolve4, reject) => {
4099
+ const cleanup = () => {
4100
+ server.off("listening", handleListening);
4101
+ server.off("error", handleError);
4102
+ };
4103
+ const handleListening = () => {
4104
+ cleanup();
4105
+ resolve4();
4106
+ };
4107
+ const handleError = (error) => {
4108
+ cleanup();
4109
+ reject(error);
4110
+ };
4111
+ server.once("listening", handleListening);
4112
+ server.once("error", handleError);
4113
+ });
3833
4114
  }
3834
- function getBaseUrlFromOptionsCredentials(credentials2) {
3835
- if (credentials2 && typeof credentials2 === "object" && "baseUrl" in credentials2 && typeof credentials2.baseUrl === "string") {
3836
- return credentials2.baseUrl;
4115
+ async function openBrowser({
4116
+ transaction,
4117
+ authAction,
4118
+ silent,
4119
+ onProgress
4120
+ }) {
4121
+ if (!silent) {
4122
+ log_default.info(`Opening your browser to ${authAction}.`);
4123
+ log_default.info("If it doesn't open, visit:", transaction.browserAuthUrl);
4124
+ }
4125
+ onProgress?.({ type: "browser_opening", url: transaction.browserAuthUrl });
4126
+ try {
4127
+ await open(transaction.browserAuthUrl);
4128
+ onProgress?.({ type: "browser_opened", url: transaction.browserAuthUrl });
4129
+ } catch (err) {
4130
+ const reason = err instanceof Error ? err.message : String(err);
4131
+ if (!silent) {
4132
+ log_default.info(
4133
+ `Browser did not open automatically to ${authAction}: ${reason}`
4134
+ );
4135
+ log_default.info("Visit this URL manually:", transaction.browserAuthUrl);
4136
+ }
4137
+ onProgress?.({
4138
+ type: "browser_open_failed",
4139
+ url: transaction.browserAuthUrl,
4140
+ reason
4141
+ });
3837
4142
  }
3838
- return void 0;
3839
4143
  }
3840
- async function resolveCredentialsBaseUrl(context) {
3841
- const resolvedCredentials = "resolvedCredentials" in context ? context.resolvedCredentials : await context.resolveCredentials?.();
3842
- return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
4144
+ async function closeServer({
4145
+ server,
4146
+ connections,
4147
+ silent
4148
+ }) {
4149
+ await new Promise((resolve4) => {
4150
+ const timeout = setTimeout(() => {
4151
+ if (!silent)
4152
+ log_default.info("Server close timed out, forcing connection shutdown...");
4153
+ connections.forEach((conn) => conn.destroy());
4154
+ resolve4();
4155
+ }, 1e3);
4156
+ server.close(() => {
4157
+ clearTimeout(timeout);
4158
+ resolve4();
4159
+ });
4160
+ });
3843
4161
  }
3844
4162
 
3845
- // src/utils/non-interactive.ts
3846
- function resolveNonInteractive(options) {
3847
- return (options.nonInteractive ?? options.skipPrompts) === true || !process.stdin.isTTY || !process.stdout.isTTY;
4163
+ // src/utils/auth/oauth-errors.ts
4164
+ var SENSITIVE_OAUTH_FIELDS = [
4165
+ "access_token",
4166
+ "refresh_token",
4167
+ "id_token",
4168
+ "client_secret",
4169
+ "code_verifier",
4170
+ "code_challenge"
4171
+ ];
4172
+ function getErrorMessage(error) {
4173
+ return error instanceof Error ? error.message : String(error);
4174
+ }
4175
+ function toCamelCase(field) {
4176
+ return field.replace(
4177
+ /_([a-z])/g,
4178
+ (_match, letter) => letter.toUpperCase()
4179
+ );
4180
+ }
4181
+ function escapeRegExp(value) {
4182
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4183
+ }
4184
+ var sensitiveOauthFieldPattern = Array.from(
4185
+ new Set(
4186
+ SENSITIVE_OAUTH_FIELDS.flatMap((field) => [field, toCamelCase(field)])
4187
+ )
4188
+ ).map(escapeRegExp).join("|");
4189
+ var sensitiveQueryParamPattern = new RegExp(
4190
+ `([?&])(${sensitiveOauthFieldPattern})(=)[^&#\\s"'<>]*`,
4191
+ "gi"
4192
+ );
4193
+ function redactSensitiveOauthErrorMessage(message) {
4194
+ return message.replace(
4195
+ sensitiveQueryParamPattern,
4196
+ (_match, prefix, key, separator) => `${prefix}${key}${separator}[REDACTED]`
4197
+ ).replace(
4198
+ new RegExp(`"(${sensitiveOauthFieldPattern})"(\\s*:\\s*)"[^"]*"`, "g"),
4199
+ (_match, key, separator) => `"${key}"${separator}"[REDACTED]"`
4200
+ );
4201
+ }
4202
+ function toRedactedOauthError(error) {
4203
+ const message = redactSensitiveOauthErrorMessage(getErrorMessage(error));
4204
+ if (error instanceof ZapierCliValidationError) {
4205
+ return new ZapierCliValidationError(message);
4206
+ }
4207
+ if (error instanceof Error) {
4208
+ const redactedError = new Error(message);
4209
+ redactedError.name = error.name;
4210
+ return redactedError;
4211
+ }
4212
+ return new ZapierCliValidationError(message);
3848
4213
  }
3849
- var LoginSchema = z.object({
3850
- timeout: z.string().optional().describe("Login timeout in seconds (default: 300)"),
3851
- useApprovals: z.boolean().optional().describe(
3852
- "Require approvals for actions performed with these credentials"
3853
- ),
3854
- nonInteractive: z.boolean().optional().describe(
3855
- "Skip interactive prompts. Uses defaults where possible; errors instead of prompting when input is required. Useful in CI, piped output, or environments where TTY detection is unreliable."
3856
- ),
3857
- /** @deprecated Use `nonInteractive` instead. */
3858
- skipPrompts: z.boolean().optional().meta({
3859
- deprecated: true,
3860
- deprecationMessage: "Use --non-interactive instead."
3861
- })
3862
- }).describe("Log in to Zapier to access your account");
3863
4214
 
3864
- // src/plugins/login/index.ts
4215
+ // src/utils/auth/account-auth.ts
4216
+ var 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?";
4217
+ var SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup` to generate a fresh signup URL and try again.";
4218
+ var HEADLESS_SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup --headless` to generate a fresh signup URL and try again.";
4219
+ function getEntryPointLabel(entryPoint) {
4220
+ return entryPoint === "signup" ? "Signup" : "Login";
4221
+ }
4222
+ function getActiveCredentialsAction(entryPoint) {
4223
+ return entryPoint === "signup" ? "continue signup" : "log in again";
4224
+ }
4225
+ function getCredentialsPromptMessage(entryPoint) {
4226
+ return entryPoint === "signup" ? "Enter a name to identify these credentials:" : "Enter a name to identify them:";
4227
+ }
4228
+ function getProfileMessage(entryPoint, email) {
4229
+ return entryPoint === "signup" ? `\u{1F464} Authenticated as ${email}` : `\u{1F464} Logged in as ${email}`;
4230
+ }
4231
+ function defaultCredentialsName(email) {
4232
+ return `${email}@${hostname()}`;
4233
+ }
4234
+ function validateCredentialsName(name) {
4235
+ const trimmedName = name.trim();
4236
+ if (!trimmedName) throw new ZapierCliValidationError("Name cannot be empty");
4237
+ return trimmedName;
4238
+ }
4239
+ async function promptCredentialsName({
4240
+ email,
4241
+ promptMessage
4242
+ }) {
4243
+ const { credentialName } = await inquirer.prompt([
4244
+ {
4245
+ type: "input",
4246
+ name: "credentialName",
4247
+ message: promptMessage,
4248
+ default: defaultCredentialsName(email),
4249
+ validate: (input) => {
4250
+ try {
4251
+ validateCredentialsName(input);
4252
+ return true;
4253
+ } catch (err) {
4254
+ return err instanceof Error ? err.message : String(err);
4255
+ }
4256
+ }
4257
+ }
4258
+ ]);
4259
+ return validateCredentialsName(credentialName);
4260
+ }
4261
+ function resolveDefaultCredentialsName({
4262
+ email
4263
+ }) {
4264
+ return validateCredentialsName(defaultCredentialsName(email));
4265
+ }
3865
4266
  function toPkceCredentials(credentials2) {
3866
4267
  if (credentials2 && isCredentialsObject(credentials2) && !("clientSecret" in credentials2)) {
3867
4268
  return {
@@ -3873,104 +4274,125 @@ function toPkceCredentials(credentials2) {
3873
4274
  }
3874
4275
  return void 0;
3875
4276
  }
3876
- async function confirmRevokeAndRelogin(activeCredentials, nonInteractive) {
3877
- if (nonInteractive) {
4277
+ function parseTimeoutSeconds(timeout) {
4278
+ if (timeout === void 0) return 300;
4279
+ const timeoutSeconds = Number(timeout);
4280
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
3878
4281
  throw new ZapierCliValidationError(
3879
- `Already logged in as "${activeCredentials.name}". Run \`logout\` first or use an interactive terminal to re-authenticate.`
4282
+ "Timeout must be a positive integer (seconds)."
3880
4283
  );
3881
4284
  }
4285
+ return timeoutSeconds;
4286
+ }
4287
+ async function promptConfirm2({
4288
+ message,
4289
+ defaultValue
4290
+ }) {
3882
4291
  const { confirmed } = await inquirer.prompt([
3883
- {
3884
- type: "confirm",
3885
- name: "confirmed",
4292
+ { type: "confirm", name: "confirmed", message, default: defaultValue }
4293
+ ]);
4294
+ return confirmed;
4295
+ }
4296
+ function promptlessCredentialResetError(credentials2) {
4297
+ throw new ZapierCliValidationError(
4298
+ `Already logged in as "${credentials2.name}". Run \`logout\` first or use an interactive terminal to re-authenticate.`
4299
+ );
4300
+ }
4301
+ function promptlessLegacyJwtUpgradeError() {
4302
+ throw new ZapierCliValidationError(
4303
+ "Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials."
4304
+ );
4305
+ }
4306
+ async function clearExistingAuthState({
4307
+ sdk,
4308
+ baseUrl: baseUrl2,
4309
+ interactive,
4310
+ entryPoint
4311
+ }) {
4312
+ const activeCredentials = getActiveCredentials({ baseUrl: baseUrl2 });
4313
+ const flowLabel = getEntryPointLabel(entryPoint);
4314
+ if (activeCredentials) {
4315
+ const confirmed = interactive ? await promptConfirm2({
4316
+ defaultValue: false,
3886
4317
  message: `You are already logged in as "${activeCredentials.name}".
3887
4318
  Logging out will delete these credentials and may interrupt other Zapier SDK or CLI sessions using them.
3888
- Log out and log in again?`,
3889
- default: false
4319
+ Log out and ${getActiveCredentialsAction(entryPoint)}?`
4320
+ }) : promptlessCredentialResetError(activeCredentials);
4321
+ if (!confirmed) {
4322
+ console.log(`${flowLabel} cancelled.`);
4323
+ return false;
3890
4324
  }
3891
- ]);
3892
- if (!confirmed) {
3893
- console.log("Login cancelled.");
3894
- return false;
3895
- }
3896
- return true;
3897
- }
3898
- async function confirmJwtMigration(nonInteractive) {
3899
- if (nonInteractive) {
3900
- throw new ZapierCliValidationError(
3901
- "Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials."
3902
- );
3903
- }
3904
- const { confirmed } = await inquirer.prompt([
3905
- {
3906
- type: "confirm",
3907
- name: "confirmed",
3908
- message: "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?",
3909
- default: true
4325
+ try {
4326
+ await revokeCredentials({
4327
+ api: sdk.context.api,
4328
+ credentials: activeCredentials
4329
+ });
4330
+ } catch {
4331
+ if (!interactive) {
4332
+ throw new ZapierCliValidationError(
4333
+ `${flowLabel} cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal.`
4334
+ );
4335
+ }
4336
+ const reset = await promptConfirm2({
4337
+ defaultValue: false,
4338
+ message: `${flowLabel} cleanup failed. Reset local session state and continue?`
4339
+ });
4340
+ if (!reset) {
4341
+ console.log(`${flowLabel} cancelled.`);
4342
+ return false;
4343
+ }
4344
+ await deleteStoredClientCredentials({
4345
+ name: activeCredentials.name,
4346
+ baseUrl: activeCredentials.baseUrl
4347
+ });
3910
4348
  }
3911
- ]);
3912
- if (!confirmed) {
3913
- console.log("Login cancelled.");
3914
- return false;
3915
- }
3916
- return true;
3917
- }
3918
- async function confirmLocalLoginReset(nonInteractive) {
3919
- if (nonInteractive) {
3920
- throw new ZapierCliValidationError(
3921
- "Login cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal."
3922
- );
3923
- }
3924
- const { confirmed } = await inquirer.prompt([
3925
- {
3926
- type: "confirm",
3927
- name: "confirmed",
3928
- message: "Login cleanup failed. Reset local session state and continue?",
3929
- default: false
4349
+ } else if (hasLegacyJwtConfig()) {
4350
+ const confirmed = interactive ? await promptConfirm2({
4351
+ defaultValue: true,
4352
+ message: LEGACY_JWT_UPGRADE_PROMPT
4353
+ }) : promptlessLegacyJwtUpgradeError();
4354
+ if (!confirmed) {
4355
+ console.log(`${flowLabel} cancelled.`);
4356
+ return false;
3930
4357
  }
3931
- ]);
3932
- if (!confirmed) {
3933
- console.log("Login cancelled.");
3934
- return false;
3935
4358
  }
3936
4359
  return true;
3937
4360
  }
3938
- function parseTimeoutSeconds(timeout) {
3939
- const timeoutSeconds = timeout ? parseInt(timeout, 10) : 300;
3940
- if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
3941
- throw new Error("Timeout must be a positive number");
3942
- }
3943
- return timeoutSeconds;
4361
+ async function getProfile(api2) {
4362
+ return api2.get("/zapier/api/v4/profile/", {
4363
+ authRequired: true
4364
+ });
3944
4365
  }
3945
- async function promptCredentialsName(email, nonInteractive) {
3946
- const fallback = `${email}@${hostname()}`;
3947
- if (nonInteractive) {
3948
- return fallback;
4366
+ async function saveClientCredentials({
4367
+ api: api2,
4368
+ name,
4369
+ credentialsBaseUrl: credentialsBaseUrl2,
4370
+ useApprovals,
4371
+ cleanupLogPrefix
4372
+ }) {
4373
+ await setupClientCredentials({
4374
+ api: api2,
4375
+ name,
4376
+ credentialsBaseUrl: credentialsBaseUrl2,
4377
+ ...useApprovals && { policy: EMPTY_POLICY }
4378
+ });
4379
+ try {
4380
+ await clearLegacyJwtState();
4381
+ } catch (err) {
4382
+ console.error(
4383
+ `[${cleanupLogPrefix}] Best-effort legacy JWT cleanup failed:`,
4384
+ err
4385
+ );
3949
4386
  }
3950
- const { credentialName } = await inquirer.prompt([
3951
- {
3952
- type: "input",
3953
- name: "credentialName",
3954
- message: "Enter a name to identify them:",
3955
- default: fallback,
3956
- validate: (input) => {
3957
- if (!input.trim()) return "Name cannot be empty";
3958
- return true;
3959
- }
3960
- }
3961
- ]);
3962
- return credentialName;
3963
4387
  }
3964
- function emitLoginSuccess({
4388
+ function emitAccountAuthSuccess({
3965
4389
  sdk,
3966
4390
  profile
3967
4391
  }) {
3968
4392
  sdk.context.eventEmission.emit(
3969
4393
  "platform.sdk.ApplicationLifecycleEvent",
3970
4394
  buildApplicationLifecycleEvent(
3971
- {
3972
- lifecycle_event_type: "login_success"
3973
- },
4395
+ { lifecycle_event_type: "login_success" },
3974
4396
  {
3975
4397
  customuser_id: profile.user_id,
3976
4398
  account_id: profile.roles[0]?.account_id ?? null
@@ -3978,18 +4400,128 @@ function emitLoginSuccess({
3978
4400
  )
3979
4401
  );
3980
4402
  }
3981
- async function getProfile(api2) {
3982
- return api2.get("/zapier/api/v4/profile/", {
3983
- authRequired: true
3984
- });
4403
+ function emitSignupSuccess({
4404
+ sdk
4405
+ }) {
4406
+ sdk.context.eventEmission.emit(
4407
+ "platform.sdk.ApplicationLifecycleEvent",
4408
+ buildApplicationLifecycleEvent({ lifecycle_event_type: "signup_success" })
4409
+ );
3985
4410
  }
3986
- async function bestEffortClearLegacyJwtState() {
4411
+ async function runOauthWithRedaction(runOauth) {
3987
4412
  try {
3988
- await clearLegacyJwtState();
3989
- } catch (err) {
3990
- console.error("[login] Best-effort legacy JWT cleanup failed:", err);
4413
+ return await runOauth();
4414
+ } catch (error) {
4415
+ if (error instanceof ZapierCliUserCancellationError) throw error;
4416
+ throw toRedactedOauthError(error);
4417
+ }
4418
+ }
4419
+ async function runOauthForEntryPoint({
4420
+ sdk,
4421
+ entryPoint,
4422
+ timeoutMs,
4423
+ pkceCredentials,
4424
+ baseUrl: baseUrl2,
4425
+ headless,
4426
+ interactive
4427
+ }) {
4428
+ if (entryPoint === "signup") {
4429
+ return runOauthWithRedaction(
4430
+ () => runSignupOauthFlow({
4431
+ timeoutMs,
4432
+ pkceCredentials,
4433
+ baseUrl: baseUrl2,
4434
+ headless,
4435
+ interactive,
4436
+ recoveryMessage: headless ? HEADLESS_SIGNUP_RECOVERY_MESSAGE : SIGNUP_RECOVERY_MESSAGE,
4437
+ onProgress: (event) => {
4438
+ if (event.type === "callback_accepted") {
4439
+ emitSignupSuccess({ sdk });
4440
+ }
4441
+ }
4442
+ })
4443
+ );
4444
+ }
4445
+ return runOauthWithRedaction(
4446
+ () => runLoginOauthFlow({ timeoutMs, pkceCredentials, baseUrl: baseUrl2 })
4447
+ );
4448
+ }
4449
+ async function runAccountAuth({
4450
+ sdk,
4451
+ options,
4452
+ entryPoint
4453
+ }) {
4454
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
4455
+ const interactive = !resolveNonInteractive(options);
4456
+ const resolvedCredentials = await sdk.context.resolveCredentials();
4457
+ const pkceCredentials = toPkceCredentials(resolvedCredentials);
4458
+ const credentialsBaseUrl2 = await resolveCredentialsBaseUrl({
4459
+ ...sdk.context,
4460
+ resolvedCredentials
4461
+ });
4462
+ if (!await clearExistingAuthState({
4463
+ sdk,
4464
+ baseUrl: credentialsBaseUrl2,
4465
+ interactive,
4466
+ entryPoint
4467
+ })) {
4468
+ return;
4469
+ }
4470
+ const { accessToken } = await runOauthForEntryPoint({
4471
+ sdk,
4472
+ entryPoint,
4473
+ timeoutMs: timeoutSeconds * 1e3,
4474
+ pkceCredentials,
4475
+ baseUrl: credentialsBaseUrl2,
4476
+ headless: options.headless === true,
4477
+ interactive
4478
+ });
4479
+ const scopedApi = getOrCreateApiClient({
4480
+ credentials: accessToken,
4481
+ baseUrl: credentialsBaseUrl2
4482
+ });
4483
+ const profile = await getProfile(scopedApi);
4484
+ console.log(getProfileMessage(entryPoint, profile.email));
4485
+ console.log(
4486
+ "\nGenerating credentials so this machine can make authenticated requests on your behalf."
4487
+ );
4488
+ const resolveCredentialsName = interactive ? ({ email }) => promptCredentialsName({
4489
+ email,
4490
+ promptMessage: getCredentialsPromptMessage(entryPoint)
4491
+ }) : resolveDefaultCredentialsName;
4492
+ const credentialName = await resolveCredentialsName({ email: profile.email });
4493
+ const useApprovals = options.useApprovals === true;
4494
+ await saveClientCredentials({
4495
+ api: scopedApi,
4496
+ name: credentialName,
4497
+ credentialsBaseUrl: credentialsBaseUrl2,
4498
+ useApprovals,
4499
+ cleanupLogPrefix: entryPoint
4500
+ });
4501
+ console.log(
4502
+ `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
4503
+ );
4504
+ if (useApprovals) {
4505
+ console.log("\u{1F510} Approvals are enabled for these credentials.");
3991
4506
  }
4507
+ emitAccountAuthSuccess({ sdk, profile });
3992
4508
  }
4509
+ var LoginSchema = z.object({
4510
+ timeout: z.string().optional().describe("Login timeout in seconds (default: 300)"),
4511
+ useApprovals: z.boolean().optional().describe(
4512
+ "Require approvals for actions performed with these credentials"
4513
+ ),
4514
+ nonInteractive: z.boolean().optional().describe(
4515
+ "Skip interactive prompts. Uses defaults where possible; errors instead of prompting when input is required. Useful in CI, piped output, or environments where TTY detection is unreliable."
4516
+ ),
4517
+ /** @deprecated Use `nonInteractive` instead. */
4518
+ skipPrompts: z.boolean().optional().meta({
4519
+ deprecated: true,
4520
+ deprecationMessage: "Use --non-interactive instead."
4521
+ })
4522
+ }).describe("Log in to Zapier to access your account");
4523
+
4524
+ // src/plugins/login/index.ts
3993
4525
  var loginPlugin = definePlugin(
3994
4526
  (sdk) => createPluginMethod(sdk, {
3995
4527
  name: "login",
@@ -3997,68 +4529,37 @@ var loginPlugin = definePlugin(
3997
4529
  inputSchema: LoginSchema,
3998
4530
  supportsJsonOutput: false,
3999
4531
  handler: async ({ sdk: sdk2, options }) => {
4000
- const timeoutSeconds = parseTimeoutSeconds(options.timeout);
4001
- const nonInteractive = resolveNonInteractive(options);
4002
- const resolvedCredentials = await sdk2.context.resolveCredentials();
4003
- const pkceCredentials = toPkceCredentials(resolvedCredentials);
4004
- const credentialsBaseUrl2 = await resolveCredentialsBaseUrl({
4005
- ...sdk2.context,
4006
- resolvedCredentials
4007
- });
4008
- const activeCredentials = getActiveCredentials({
4009
- baseUrl: credentialsBaseUrl2
4010
- });
4011
- if (activeCredentials) {
4012
- if (!await confirmRevokeAndRelogin(activeCredentials, nonInteractive))
4013
- return;
4014
- try {
4015
- await revokeCredentials({
4016
- api: sdk2.context.api,
4017
- credentials: activeCredentials
4018
- });
4019
- } catch {
4020
- if (!await confirmLocalLoginReset(nonInteractive)) return;
4021
- await deleteStoredClientCredentials({
4022
- name: activeCredentials.name,
4023
- baseUrl: activeCredentials.baseUrl
4024
- });
4025
- }
4026
- } else if (hasLegacyJwtConfig()) {
4027
- if (!await confirmJwtMigration(nonInteractive)) return;
4028
- }
4029
- const { accessToken } = await runOauthFlow({
4030
- timeoutMs: timeoutSeconds * 1e3,
4031
- pkceCredentials,
4032
- baseUrl: credentialsBaseUrl2
4033
- });
4034
- const scopedApi = getOrCreateApiClient({
4035
- credentials: accessToken,
4036
- baseUrl: credentialsBaseUrl2
4037
- });
4038
- const profile = await getProfile(scopedApi);
4039
- console.log(`\u{1F464} Logged in as ${profile.email}`);
4040
- console.log(
4041
- "\nGenerating credentials so this machine can make authenticated requests on your behalf."
4042
- );
4043
- const credentialName = await promptCredentialsName(
4044
- profile.email,
4045
- nonInteractive
4046
- );
4047
- const useApprovals = options.useApprovals === true;
4048
- await setupClientCredentials({
4049
- api: scopedApi,
4050
- name: credentialName,
4051
- credentialsBaseUrl: credentialsBaseUrl2,
4052
- ...useApprovals && { policy: EMPTY_POLICY }
4053
- });
4054
- await bestEffortClearLegacyJwtState();
4055
- console.log(
4056
- `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
4057
- );
4058
- if (useApprovals) {
4059
- console.log("\u{1F510} Approvals are enabled for these credentials.");
4060
- }
4061
- emitLoginSuccess({ sdk: sdk2, profile });
4532
+ await runAccountAuth({ sdk: sdk2, options, entryPoint: "login" });
4533
+ }
4534
+ })
4535
+ );
4536
+ var SignupSchema = z.object({
4537
+ timeout: z.string().optional().describe("Signup timeout in seconds (default: 300)"),
4538
+ useApprovals: z.boolean().optional().describe(
4539
+ "Require approvals for actions performed with these credentials"
4540
+ ),
4541
+ nonInteractive: z.boolean().optional().describe(
4542
+ "Skip interactive prompts. Uses defaults where possible; errors instead of prompting when input is required. Useful in CI, piped output, or environments where TTY detection is unreliable."
4543
+ ),
4544
+ /** @deprecated Use `nonInteractive` instead. */
4545
+ skipPrompts: z.boolean().optional().meta({
4546
+ deprecated: true,
4547
+ deprecationMessage: "Use --non-interactive instead."
4548
+ }),
4549
+ headless: z.boolean().optional().describe(
4550
+ "Use when signing up from a machine that has no browser. Prints a signup link to open elsewhere, then accepts the pasted loopback callback URL."
4551
+ )
4552
+ }).describe("Set up Zapier account access and SDK credentials");
4553
+
4554
+ // src/plugins/signup/index.ts
4555
+ var signupPlugin = definePlugin(
4556
+ (sdk) => createPluginMethod(sdk, {
4557
+ name: "signup",
4558
+ categories: ["account"],
4559
+ inputSchema: SignupSchema,
4560
+ supportsJsonOutput: false,
4561
+ handler: async ({ sdk: sdk2, options }) => {
4562
+ await runAccountAuth({ sdk: sdk2, options, entryPoint: "signup" });
4062
4563
  }
4063
4564
  })
4064
4565
  );
@@ -6607,7 +7108,7 @@ var watchTriggerInboxCliPlugin = definePlugin(
6607
7108
  // package.json with { type: 'json' }
6608
7109
  var package_default2 = {
6609
7110
  name: "@zapier/zapier-sdk-cli",
6610
- version: "0.52.12"};
7111
+ version: "0.53.0"};
6611
7112
 
6612
7113
  // src/sdk.ts
6613
7114
  injectCliLogin(login_exports);
@@ -6620,7 +7121,7 @@ function createZapierCliSdk(options = {}) {
6620
7121
  ...sdkOptions,
6621
7122
  eventEmission: { ...sdkOptions.eventEmission, callContext: "cli" },
6622
7123
  callerPackage: { name: package_default2.name, version: package_default2.version }
6623
- }).use(extensionsContextPlugin).use(generateAppTypesPlugin).use(buildManifestPlugin).use(bundleCodePlugin).use(getLoginConfigPathPlugin).use(addAppsPlugin).use(feedbackPlugin).use(curlPlugin).use(initPlugin).use(mcpPlugin).use(loginPlugin).use(logoutPlugin).use(cliOverridesPlugin, { override: true }).toSdk();
7124
+ }).use(extensionsContextPlugin).use(generateAppTypesPlugin).use(buildManifestPlugin).use(bundleCodePlugin).use(getLoginConfigPathPlugin).use(addAppsPlugin).use(feedbackPlugin).use(curlPlugin).use(initPlugin).use(mcpPlugin).use(loginPlugin).use(signupPlugin).use(logoutPlugin).use(cliOverridesPlugin, { override: true }).toSdk();
6624
7125
  for (const ext of extensions) {
6625
7126
  try {
6626
7127
  addPlugin$1(sdk, ext);
@@ -6645,7 +7146,7 @@ function createZapierCliSdk2(options = {}) {
6645
7146
  ...sdkOptions,
6646
7147
  eventEmission: { ...sdkOptions.eventEmission, callContext: "cli" },
6647
7148
  callerPackage: { name: package_default2.name, version: package_default2.version }
6648
- }).use(extensionsContextPlugin).use(experimentalContextPlugin).use(generateAppTypesPlugin).use(buildManifestPlugin).use(bundleCodePlugin).use(getLoginConfigPathPlugin).use(addAppsPlugin).use(feedbackPlugin).use(curlPlugin).use(initPlugin).use(drainTriggerInboxCliPlugin, { override: true }).use(watchTriggerInboxCliPlugin, { override: true }).use(mcpPlugin).use(loginPlugin).use(logoutPlugin).use(cliOverridesPlugin, { override: true }).toSdk();
7149
+ }).use(extensionsContextPlugin).use(experimentalContextPlugin).use(generateAppTypesPlugin).use(buildManifestPlugin).use(bundleCodePlugin).use(getLoginConfigPathPlugin).use(addAppsPlugin).use(feedbackPlugin).use(curlPlugin).use(initPlugin).use(drainTriggerInboxCliPlugin, { override: true }).use(watchTriggerInboxCliPlugin, { override: true }).use(mcpPlugin).use(loginPlugin).use(signupPlugin).use(logoutPlugin).use(cliOverridesPlugin, { override: true }).toSdk();
6649
7150
  for (const ext of extensions) {
6650
7151
  try {
6651
7152
  addPlugin(sdk, ext);