@zapier/zapier-sdk-cli 0.52.10 → 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 (71) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +39 -1
  3. package/dist/cli.cjs +972 -473
  4. package/dist/cli.mjs +973 -474
  5. package/dist/experimental.cjs +914 -424
  6. package/dist/experimental.d.mts +1 -1
  7. package/dist/experimental.d.ts +1 -1
  8. package/dist/experimental.mjs +910 -420
  9. package/dist/index.cjs +914 -424
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.mjs +910 -420
  13. package/dist/login.cjs +8 -10
  14. package/dist/login.d.mts +2 -10
  15. package/dist/login.d.ts +2 -10
  16. package/dist/login.mjs +5 -9
  17. package/dist/package.json +1 -1
  18. package/dist/{sdk-B3nKAZdN.d.mts → sdk-SOLizjno.d.mts} +54 -16
  19. package/dist/{sdk-B3nKAZdN.d.ts → sdk-SOLizjno.d.ts} +54 -16
  20. package/dist/src/experimental.js +30 -27
  21. package/dist/src/login/index.d.ts +1 -9
  22. package/dist/src/login/index.js +12 -14
  23. package/dist/src/plugins/add/index.d.ts +15 -15
  24. package/dist/src/plugins/add/index.js +1 -1
  25. package/dist/src/plugins/buildManifest/index.d.ts +2 -2
  26. package/dist/src/plugins/bundleCode/index.d.ts +1 -1
  27. package/dist/src/plugins/bundleCode/index.js +2 -1
  28. package/dist/src/plugins/cliOverrides/index.d.ts +5 -10
  29. package/dist/src/plugins/cliOverrides/index.js +2 -6
  30. package/dist/src/plugins/curl/index.d.ts +2 -2
  31. package/dist/src/plugins/curl/schemas.d.ts +2 -2
  32. package/dist/src/plugins/feedback/index.d.ts +1 -1
  33. package/dist/src/plugins/generateAppTypes/index.d.ts +11 -11
  34. package/dist/src/plugins/getLoginConfigPath/index.d.ts +1 -1
  35. package/dist/src/plugins/index.d.ts +2 -1
  36. package/dist/src/plugins/index.js +2 -1
  37. package/dist/src/plugins/init/index.d.ts +1 -1
  38. package/dist/src/plugins/login/index.d.ts +3 -16
  39. package/dist/src/plugins/login/index.js +3 -191
  40. package/dist/src/plugins/logout/index.d.ts +1 -1
  41. package/dist/src/plugins/mcp/index.d.ts +1 -1
  42. package/dist/src/plugins/signup/index.d.ts +25 -0
  43. package/dist/src/plugins/signup/index.js +12 -0
  44. package/dist/src/plugins/signup/schemas.d.ts +9 -0
  45. package/dist/src/plugins/signup/schemas.js +26 -0
  46. package/dist/src/plugins/signup/test-harness.d.ts +34 -0
  47. package/dist/src/plugins/signup/test-harness.js +74 -0
  48. package/dist/src/sdk.js +32 -20
  49. package/dist/src/types/sdk.d.ts +2 -1
  50. package/dist/src/utils/auth/account-auth.d.ts +32 -0
  51. package/dist/src/utils/auth/account-auth.js +265 -0
  52. package/dist/src/utils/auth/oauth-callback.d.ts +6 -0
  53. package/dist/src/utils/auth/oauth-callback.js +28 -0
  54. package/dist/src/utils/auth/oauth-errors.d.ts +2 -0
  55. package/dist/src/utils/auth/oauth-errors.js +39 -0
  56. package/dist/src/utils/auth/oauth-flow.d.ts +31 -6
  57. package/dist/src/utils/auth/oauth-flow.js +258 -106
  58. package/dist/src/utils/auth/oauth-transaction.d.ts +35 -0
  59. package/dist/src/utils/auth/oauth-transaction.js +69 -0
  60. package/dist/src/utils/cli-generator.js +14 -7
  61. package/dist/src/utils/cli-renderer.d.ts +13 -3
  62. package/dist/src/utils/cli-renderer.js +27 -20
  63. package/dist/src/utils/log.js +9 -4
  64. package/dist/src/utils/non-interactive.d.ts +5 -4
  65. package/dist/src/utils/non-interactive.js +6 -5
  66. package/dist/src/utils/parameter-resolver.js +3 -1
  67. package/dist/src/utils/schema-formatter.d.ts +2 -2
  68. package/dist/src/utils/schema-formatter.js +4 -30
  69. package/dist/src/utils/version-checker.js +8 -3
  70. package/dist/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as jwt from 'jsonwebtoken';
2
- import { deletePassword, getKeyring, getPassword, setPassword } from 'cross-keychain';
2
+ import { deletePassword, setPassword, getKeyring, getPassword } from 'cross-keychain';
3
3
  import Conf from 'conf';
4
4
  import * as fs from 'fs';
5
5
  import { promises, createWriteStream, existsSync, readdirSync, rmSync, mkdirSync, writeFileSync, copyFileSync, readFileSync } from 'fs';
@@ -7,15 +7,16 @@ import crypto, { createHash } from 'crypto';
7
7
  import * as path from 'path';
8
8
  import { resolve, join, dirname, basename, relative, extname } from 'path';
9
9
  import * as lockfile from 'proper-lockfile';
10
+ import { definePlugin, createPluginMethod, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, getOrCreateApiClient, invalidateCachedToken, batch, toSnakeCase, ZapierAbortDrainSignal, isCredentialsObject, buildApplicationLifecycleEvent, ZapierAuthenticationError, ZapierError, createZapierSdkStack, addPlugin, getOsInfo, getPlatformVersions, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId } from '@zapier/zapier-sdk';
10
11
  import { z } from 'zod';
11
- import { definePlugin, createPluginMethod, getOrCreateApiClient, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, injectCliLogin, isCredentialsObject, invalidateCachedToken, buildApplicationLifecycleEvent, batch, toSnakeCase, ZapierAbortDrainSignal, ZapierError, createZapierSdk, getOsInfo, getPlatformVersions, getCiPlatform, isCi, getReleaseId, getCurrentTimestamp, generateEventId } from '@zapier/zapier-sdk';
12
12
  import { hostname } from 'os';
13
13
  import inquirer from 'inquirer';
14
- import open from 'open';
15
14
  import express from 'express';
16
- import pkceChallenge from 'pkce-challenge';
17
- import ora from 'ora';
15
+ import { createInterface } from 'readline/promises';
16
+ import open from 'open';
18
17
  import chalk3 from 'chalk';
18
+ import ora from 'ora';
19
+ import pkceChallenge from 'pkce-challenge';
19
20
  import { startMcpServer } from '@zapier/zapier-sdk-mcp';
20
21
  import { buildSync } from 'esbuild';
21
22
  import { mkdir, writeFile, access } from 'fs/promises';
@@ -441,12 +442,6 @@ async function deleteStoredClientCredentials({
441
442
  }
442
443
 
443
444
  // src/login/index.ts
444
- var ZapierAuthenticationError = class extends Error {
445
- constructor(message) {
446
- super(message);
447
- this.name = "ZapierAuthenticationError";
448
- }
449
- };
450
445
  var DEFAULT_AUTH_CLIENT_ID = "grwWZD5hUWGvb4V8ODBuOtXer3h0DBEZ2HR8aay6";
451
446
  var TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
452
447
  function createDebugLog(enabled) {
@@ -456,9 +451,9 @@ function createDebugLog(enabled) {
456
451
  }
457
452
  return (message, data) => {
458
453
  if (data === void 0) {
459
- console.log(`[Zapier SDK CLI Login] ${message}`);
454
+ console.error(`[Zapier SDK CLI Login] ${message}`);
460
455
  } else {
461
- console.log(`[Zapier SDK CLI Login] ${message}`, data);
456
+ console.error(`[Zapier SDK CLI Login] ${message}`, data);
462
457
  }
463
458
  };
464
459
  }
@@ -857,10 +852,27 @@ async function revokeCredentials({
857
852
  });
858
853
  emitAuthLogout(onEvent);
859
854
  }
855
+ function getBaseUrlFromResolvedCredentials(credentials) {
856
+ if (credentials && isCredentialsObject(credentials)) {
857
+ return credentials.baseUrl;
858
+ }
859
+ return void 0;
860
+ }
861
+ function getBaseUrlFromOptionsCredentials(credentials) {
862
+ if (credentials && typeof credentials === "object" && "baseUrl" in credentials && typeof credentials.baseUrl === "string") {
863
+ return credentials.baseUrl;
864
+ }
865
+ return void 0;
866
+ }
867
+ async function resolveCredentialsBaseUrl(context) {
868
+ const resolvedCredentials = "resolvedCredentials" in context ? context.resolvedCredentials : await context.resolveCredentials?.();
869
+ return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
870
+ }
860
871
 
861
- // src/utils/constants.ts
862
- var LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
863
- var LOGIN_TIMEOUT_MS = 3e5;
872
+ // src/utils/non-interactive.ts
873
+ function resolveNonInteractive(options) {
874
+ return options.nonInteractive === true || options.skipPrompts === true || !process.stdin.isTTY || !process.stdout.isTTY;
875
+ }
864
876
  var ZapierCliError = class extends ZapierError {
865
877
  };
866
878
  var ZapierCliUserCancellationError = class extends ZapierCliError {
@@ -888,42 +900,164 @@ var ZapierCliValidationError = class extends ZapierCliError {
888
900
  }
889
901
  };
890
902
 
891
- // src/utils/spinner.ts
892
- var spinPromise = async (promise, text) => {
893
- const spinner = ora(text).start();
903
+ // src/utils/auth/client-credentials.ts
904
+ var CREDENTIALS_SCOPES = ["external", "credentials"];
905
+ var EMPTY_POLICY = {
906
+ version: 2,
907
+ statements: []
908
+ };
909
+ async function createCredentialsOnServer(api2, name, policy) {
910
+ const response = await api2.post(
911
+ "/api/v0/client-credentials",
912
+ {
913
+ name,
914
+ allowed_scopes: CREDENTIALS_SCOPES,
915
+ ...policy !== void 0 && { policy }
916
+ },
917
+ { authRequired: true, requiredScopes: ["credentials"] }
918
+ );
919
+ return {
920
+ clientId: response.data.client_id,
921
+ clientSecret: response.data.client_secret
922
+ };
923
+ }
924
+ async function deleteCredentialsOnServer(api2, clientId) {
925
+ await api2.delete(`/api/v0/client-credentials/${clientId}`, void 0, {
926
+ authRequired: true,
927
+ requiredScopes: ["credentials"]
928
+ });
929
+ }
930
+ async function setupClientCredentials({
931
+ api: api2,
932
+ name,
933
+ credentialsBaseUrl,
934
+ policy
935
+ }) {
936
+ const { clientId, clientSecret } = await createCredentialsOnServer(
937
+ api2,
938
+ name,
939
+ policy
940
+ );
894
941
  try {
895
- const result = await promise;
896
- spinner.succeed();
897
- return result;
898
- } catch (error) {
899
- if (error instanceof ZapierCliUserCancellationError) {
900
- spinner.stop();
901
- } else {
902
- spinner.fail();
942
+ await withRetry({
943
+ action: () => storeClientCredentials({
944
+ name,
945
+ clientId,
946
+ clientSecret,
947
+ scopes: [...CREDENTIALS_SCOPES],
948
+ baseUrl: credentialsBaseUrl
949
+ })
950
+ });
951
+ } catch (storeErr) {
952
+ try {
953
+ await withRetry({
954
+ action: () => deleteCredentialsOnServer(api2, clientId)
955
+ });
956
+ } catch {
957
+ console.error(
958
+ `Failed to roll back orphaned credential ${clientId}. Delete it manually with: zapier-sdk delete-client-credentials ${clientId}`
959
+ );
903
960
  }
904
- throw error;
961
+ throw storeErr;
905
962
  }
963
+ return { clientId };
964
+ }
965
+
966
+ // src/utils/constants.ts
967
+ var LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
968
+ var LOGIN_TIMEOUT_MS = 3e5;
969
+
970
+ // src/utils/getCallablePromise.ts
971
+ var getCallablePromise = () => {
972
+ let resolve4 = () => {
973
+ };
974
+ let reject = () => {
975
+ };
976
+ const promise = new Promise((_resolve, _reject) => {
977
+ resolve4 = _resolve;
978
+ reject = _reject;
979
+ });
980
+ return {
981
+ promise,
982
+ resolve: resolve4,
983
+ reject
984
+ };
906
985
  };
986
+ var getCallablePromise_default = getCallablePromise;
907
987
  var log = {
908
988
  info: (message, ...args) => {
909
- console.log(chalk3.blue("\u2139"), message, ...args);
989
+ console.error(chalk3.blue("\u2139"), message, ...args);
910
990
  },
911
991
  error: (message, ...args) => {
912
992
  console.error(chalk3.red("\u2716"), message, ...args);
913
993
  },
914
994
  success: (message, ...args) => {
915
- console.log(chalk3.green("\u2713"), message, ...args);
995
+ console.error(chalk3.green("\u2713"), message, ...args);
916
996
  },
917
997
  warn: (message, ...args) => {
918
- console.log(chalk3.yellow("\u26A0"), message, ...args);
998
+ console.error(chalk3.yellow("\u26A0"), message, ...args);
919
999
  },
920
1000
  debug: (message, ...args) => {
921
1001
  if (process.env.DEBUG === "true" || process.argv.includes("--debug")) {
922
- console.log(chalk3.gray("\u{1F41B}"), message, ...args);
1002
+ console.error(chalk3.gray("\u{1F41B}"), message, ...args);
923
1003
  }
924
1004
  }
925
1005
  };
926
1006
  var log_default = log;
1007
+ var spinPromise = async (promise, text) => {
1008
+ const spinner = ora(text).start();
1009
+ try {
1010
+ const result = await promise;
1011
+ spinner.succeed();
1012
+ return result;
1013
+ } catch (error) {
1014
+ if (error instanceof ZapierCliUserCancellationError) {
1015
+ spinner.stop();
1016
+ } else {
1017
+ spinner.fail();
1018
+ }
1019
+ throw error;
1020
+ }
1021
+ };
1022
+
1023
+ // src/utils/auth/oauth-callback.ts
1024
+ function getCallbackCode({
1025
+ callbackUrl,
1026
+ transaction,
1027
+ recoveryMessage
1028
+ }) {
1029
+ let parsed;
1030
+ try {
1031
+ parsed = new URL(callbackUrl.trim());
1032
+ } catch {
1033
+ throw new ZapierCliValidationError(
1034
+ "Paste the final OAuth callback URL from your browser."
1035
+ );
1036
+ }
1037
+ const expected = new URL(transaction.redirectUri);
1038
+ if (parsed.protocol !== "http:" || parsed.hostname !== expected.hostname || parsed.pathname !== expected.pathname || parsed.port !== expected.port) {
1039
+ throw new ZapierCliValidationError(
1040
+ `Expected the final OAuth callback URL to start with ${transaction.redirectUri}.`
1041
+ );
1042
+ }
1043
+ if (parsed.searchParams.get("state") !== transaction.state) {
1044
+ throw new ZapierCliValidationError(
1045
+ `OAuth state mismatch.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
1046
+ );
1047
+ }
1048
+ if (parsed.searchParams.has("error")) {
1049
+ throw new ZapierCliValidationError(
1050
+ `Authorization denied: ${parsed.searchParams.get("error_description") ?? parsed.searchParams.get("error")}.${recoveryMessage ? ` ${recoveryMessage}` : ""}`
1051
+ );
1052
+ }
1053
+ const code = parsed.searchParams.get("code");
1054
+ if (!code) {
1055
+ throw new ZapierCliValidationError(
1056
+ "No authorization code found in the pasted callback URL."
1057
+ );
1058
+ }
1059
+ return code;
1060
+ }
927
1061
 
928
1062
  // src/utils/api/client.ts
929
1063
  var createApiClient = () => {
@@ -954,113 +1088,351 @@ var createApiClient = () => {
954
1088
  var api = createApiClient();
955
1089
  var client_default = api;
956
1090
 
957
- // src/utils/getCallablePromise.ts
958
- var getCallablePromise = () => {
959
- let resolve4 = () => {
960
- };
961
- let reject = () => {
962
- };
963
- const promise = new Promise((_resolve, _reject) => {
964
- resolve4 = _resolve;
965
- reject = _reject;
1091
+ // src/utils/auth/oauth-transaction.ts
1092
+ var OAUTH_LOOPBACK_HOST = "localhost";
1093
+ function buildBrowserAuthUrl({
1094
+ authorizeUrl,
1095
+ entryPoint = "login"
1096
+ }) {
1097
+ if (entryPoint === "login") return authorizeUrl;
1098
+ const parsedAuthorizeUrl = new URL(authorizeUrl);
1099
+ const signupUrl = new URL("/sign-up", parsedAuthorizeUrl);
1100
+ signupUrl.searchParams.set("skipOnboarding", "true");
1101
+ signupUrl.searchParams.set(
1102
+ "next",
1103
+ `${parsedAuthorizeUrl.pathname}${parsedAuthorizeUrl.search}`
1104
+ );
1105
+ return signupUrl.toString();
1106
+ }
1107
+ function generateRandomString() {
1108
+ const array = new Uint32Array(28);
1109
+ crypto.getRandomValues(array);
1110
+ return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join(
1111
+ ""
1112
+ );
1113
+ }
1114
+ function ensureOfflineAccess(scope) {
1115
+ if (scope.includes("offline_access")) return scope;
1116
+ return `${scope} offline_access`;
1117
+ }
1118
+ async function prepareOauthTransaction({
1119
+ pkceCredentials,
1120
+ baseUrl,
1121
+ redirectUri,
1122
+ entryPoint = "login"
1123
+ }) {
1124
+ const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
1125
+ credentials: pkceCredentials,
1126
+ baseUrl
966
1127
  });
1128
+ const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
1129
+ const state = generateRandomString();
1130
+ const authUrl = `${authorizeUrl}?${new URLSearchParams({
1131
+ response_type: "code",
1132
+ client_id: clientId,
1133
+ redirect_uri: redirectUri,
1134
+ scope: ensureOfflineAccess(
1135
+ pkceCredentials?.scope || "internal credentials"
1136
+ ),
1137
+ state,
1138
+ code_challenge: codeChallenge,
1139
+ code_challenge_method: "S256"
1140
+ }).toString()}`;
967
1141
  return {
968
- promise,
969
- resolve: resolve4,
970
- reject
1142
+ browserAuthUrl: buildBrowserAuthUrl({ authorizeUrl: authUrl, entryPoint }),
1143
+ clientId,
1144
+ codeVerifier,
1145
+ redirectUri,
1146
+ state,
1147
+ tokenUrl
971
1148
  };
972
- };
973
- var getCallablePromise_default = getCallablePromise;
1149
+ }
1150
+ async function exchangeOauthCode({
1151
+ tokenUrl,
1152
+ code,
1153
+ redirectUri,
1154
+ clientId,
1155
+ codeVerifier
1156
+ }) {
1157
+ const { data } = await client_default.post(
1158
+ tokenUrl,
1159
+ {
1160
+ grant_type: "authorization_code",
1161
+ code,
1162
+ redirect_uri: redirectUri,
1163
+ client_id: clientId,
1164
+ code_verifier: codeVerifier
1165
+ },
1166
+ {
1167
+ headers: {
1168
+ [AUTH_MODE_HEADER]: "no",
1169
+ "Content-Type": "application/x-www-form-urlencoded"
1170
+ }
1171
+ }
1172
+ );
1173
+ return {
1174
+ accessToken: data.access_token,
1175
+ refreshToken: data.refresh_token,
1176
+ expiresIn: data.expires_in
1177
+ };
1178
+ }
974
1179
 
975
1180
  // src/utils/auth/oauth-flow.ts
976
- var findAvailablePort = () => {
1181
+ var OauthFlowTimeoutError = class extends Error {
1182
+ constructor(timeoutMs) {
1183
+ super("OAuth flow timed out");
1184
+ this.timeoutMs = timeoutMs;
1185
+ this.name = "OauthFlowTimeoutError";
1186
+ }
1187
+ };
1188
+ var OauthAuthorizationDeniedError = class extends Error {
1189
+ constructor(reason) {
1190
+ super("OAuth authorization denied");
1191
+ this.reason = reason;
1192
+ this.name = "OauthAuthorizationDeniedError";
1193
+ }
1194
+ };
1195
+ function findAvailablePort() {
977
1196
  return new Promise((resolve4, reject) => {
978
1197
  let portIndex = 0;
979
1198
  const tryPort = (port) => {
980
- const server = express().listen(port, () => {
1199
+ const server = express().listen(port, OAUTH_LOOPBACK_HOST, () => {
981
1200
  server.close();
982
1201
  resolve4(port);
983
1202
  });
984
1203
  server.on("error", (err) => {
985
- if (err.code === "EADDRINUSE") {
986
- if (portIndex < LOGIN_PORTS.length) {
987
- tryPort(LOGIN_PORTS[portIndex++]);
988
- } else {
989
- reject(
990
- new Error(
991
- `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
992
- )
993
- );
994
- }
1204
+ if (err.code === "EADDRINUSE" && portIndex < LOGIN_PORTS.length) {
1205
+ tryPort(LOGIN_PORTS[portIndex++]);
1206
+ } else if (err.code === "EADDRINUSE") {
1207
+ reject(
1208
+ new Error(
1209
+ `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
1210
+ )
1211
+ );
995
1212
  } else {
996
1213
  reject(err);
997
1214
  }
998
1215
  });
999
1216
  };
1000
- if (LOGIN_PORTS.length > 0) {
1001
- tryPort(LOGIN_PORTS[portIndex++]);
1002
- } else {
1003
- reject(new Error("No OAuth callback ports configured"));
1217
+ if (LOGIN_PORTS.length > 0) tryPort(LOGIN_PORTS[portIndex++]);
1218
+ else reject(new Error("No OAuth callback ports configured"));
1219
+ });
1220
+ }
1221
+ async function runLoginOauthFlow(options) {
1222
+ return runOauthFlowEntryPoint({
1223
+ ...options,
1224
+ entryPoint: "login",
1225
+ authAction: "log in",
1226
+ flowName: "Login"
1227
+ });
1228
+ }
1229
+ async function runSignupOauthFlow(options) {
1230
+ if (options.headless) {
1231
+ return runOauthFlowEntryPoint({
1232
+ ...options,
1233
+ entryPoint: "signup",
1234
+ authAction: "sign up",
1235
+ flowName: "Signup",
1236
+ headless: true
1237
+ });
1238
+ }
1239
+ return runOauthFlowEntryPoint({
1240
+ ...options,
1241
+ entryPoint: "signup",
1242
+ authAction: "sign up",
1243
+ flowName: "Signup"
1244
+ });
1245
+ }
1246
+ async function runOauthFlowEntryPoint({
1247
+ flowName,
1248
+ ...options
1249
+ }) {
1250
+ try {
1251
+ return options.headless ? await runHeadlessSignupOauthFlow(options) : await runOauthFlow(options);
1252
+ } catch (error) {
1253
+ if (error instanceof OauthFlowTimeoutError) {
1254
+ throw new Error(
1255
+ withRecoveryMessage(
1256
+ `${flowName} timed out after ${Math.round(error.timeoutMs / 1e3)} seconds.`,
1257
+ options.recoveryMessage
1258
+ )
1259
+ );
1260
+ }
1261
+ if (error instanceof OauthAuthorizationDeniedError) {
1262
+ throw new Error(
1263
+ withRecoveryMessage(
1264
+ `Authorization denied: ${error.reason}.`,
1265
+ options.recoveryMessage
1266
+ )
1267
+ );
1268
+ }
1269
+ if (error instanceof ZapierCliUserCancellationError && !options.silent) {
1270
+ log_default.info(`
1271
+ \u274C ${flowName} cancelled by user`);
1004
1272
  }
1273
+ throw error;
1274
+ }
1275
+ }
1276
+ function withRecoveryMessage(message, recoveryMessage) {
1277
+ return recoveryMessage ? `${message} ${recoveryMessage}` : message;
1278
+ }
1279
+ async function runOauthFlow({
1280
+ timeoutMs = LOGIN_TIMEOUT_MS,
1281
+ pkceCredentials,
1282
+ baseUrl,
1283
+ entryPoint,
1284
+ authAction,
1285
+ silent = false,
1286
+ onProgress
1287
+ }) {
1288
+ const port = await findAvailablePort();
1289
+ if (!silent) log_default.info(`Using port ${port} for OAuth callback`);
1290
+ const transaction = await prepareOauthTransaction({
1291
+ pkceCredentials,
1292
+ baseUrl,
1293
+ redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
1294
+ entryPoint
1005
1295
  });
1006
- };
1007
- var generateRandomString = () => {
1008
- const array = new Uint32Array(28);
1009
- crypto.getRandomValues(array);
1010
- return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join(
1011
- ""
1296
+ const code = await collectLocalCallbackCode({
1297
+ transaction,
1298
+ timeoutMs,
1299
+ authAction,
1300
+ silent,
1301
+ onProgress
1302
+ });
1303
+ onProgress?.({ type: "callback_accepted" });
1304
+ if (!silent) log_default.info("Exchanging authorization code for tokens...");
1305
+ onProgress?.({ type: "token_exchange_started" });
1306
+ const tokens = await exchangeOauthCode({ ...transaction, code });
1307
+ if (!silent) log_default.info("Token exchange completed successfully");
1308
+ onProgress?.({ type: "token_exchange_completed" });
1309
+ return tokens;
1310
+ }
1311
+ async function readHeadlessCallbackUrl({
1312
+ timeoutMs,
1313
+ interactive,
1314
+ recoveryMessage
1315
+ }) {
1316
+ const timeoutMessage = withRecoveryMessage(
1317
+ `Signup timed out after ${Math.round(timeoutMs / 1e3)} seconds.`,
1318
+ recoveryMessage
1012
1319
  );
1013
- };
1014
- function ensureOfflineAccess(scope) {
1015
- if (scope.includes("offline_access")) {
1016
- return scope;
1320
+ const missingCallbackUrlMessage = withRecoveryMessage(
1321
+ "Paste the final OAuth callback URL from your browser.",
1322
+ recoveryMessage
1323
+ );
1324
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1325
+ const abortController = new AbortController();
1326
+ const timeoutTimer = setTimeout(() => abortController.abort(), timeoutMs);
1327
+ const readUrl = interactive ? rl.question("Paste the final OAuth callback URL: ", {
1328
+ signal: abortController.signal
1329
+ }) : new Promise((resolve4, reject) => {
1330
+ let settled = false;
1331
+ const settleResolve = (value) => {
1332
+ settled = true;
1333
+ resolve4(value);
1334
+ };
1335
+ const settleReject = (error) => {
1336
+ if (settled) return;
1337
+ settled = true;
1338
+ reject(error);
1339
+ };
1340
+ abortController.signal.addEventListener(
1341
+ "abort",
1342
+ () => settleReject(new Error(timeoutMessage)),
1343
+ { once: true }
1344
+ );
1345
+ rl.once("line", settleResolve);
1346
+ rl.once(
1347
+ "close",
1348
+ () => settleReject(new ZapierCliValidationError(missingCallbackUrlMessage))
1349
+ );
1350
+ rl.once("error", settleReject);
1351
+ });
1352
+ try {
1353
+ return await readUrl.catch((error) => {
1354
+ if (error instanceof Error && error.name === "AbortError") {
1355
+ throw new Error(timeoutMessage);
1356
+ }
1357
+ throw error;
1358
+ });
1359
+ } finally {
1360
+ clearTimeout(timeoutTimer);
1361
+ rl.close();
1017
1362
  }
1018
- return `${scope} offline_access`;
1019
1363
  }
1020
- async function runOauthFlow({
1364
+ async function runHeadlessSignupOauthFlow({
1021
1365
  timeoutMs = LOGIN_TIMEOUT_MS,
1022
1366
  pkceCredentials,
1023
- baseUrl
1367
+ baseUrl,
1368
+ interactive = true,
1369
+ onProgress,
1370
+ recoveryMessage
1024
1371
  }) {
1025
- const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
1026
- credentials: pkceCredentials,
1027
- baseUrl
1372
+ const port = LOGIN_PORTS[0];
1373
+ const transaction = await prepareOauthTransaction({
1374
+ pkceCredentials,
1375
+ baseUrl,
1376
+ redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
1377
+ entryPoint: "signup"
1028
1378
  });
1029
- const scope = ensureOfflineAccess(
1030
- pkceCredentials?.scope || "internal credentials"
1379
+ console.log(
1380
+ "Use this mode when signing up from a machine that has no browser."
1031
1381
  );
1032
- const availablePort = await findAvailablePort();
1033
- const redirectUri = `http://localhost:${availablePort}/oauth`;
1034
- log_default.info(`Using port ${availablePort} for OAuth callback`);
1035
- const {
1036
- promise: promisedCode,
1037
- resolve: setCode,
1038
- reject: rejectCode
1039
- } = getCallablePromise_default();
1040
- const oauthState = generateRandomString();
1041
- const expressApp = express();
1042
- expressApp.get("/oauth", (req, res) => {
1382
+ console.log("Open this signup URL in a browser on another machine:");
1383
+ console.log(transaction.browserAuthUrl);
1384
+ console.log(
1385
+ `When the browser lands on ${transaction.redirectUri} and cannot connect, paste the full final URL back here.`
1386
+ );
1387
+ const callbackUrl = await readHeadlessCallbackUrl({
1388
+ timeoutMs,
1389
+ interactive,
1390
+ recoveryMessage
1391
+ });
1392
+ const code = getCallbackCode({
1393
+ callbackUrl,
1394
+ transaction,
1395
+ recoveryMessage
1396
+ });
1397
+ onProgress?.({ type: "callback_accepted" });
1398
+ console.log("Exchanging authorization code for tokens...");
1399
+ onProgress?.({ type: "token_exchange_started" });
1400
+ const tokens = await exchangeOauthCode({ ...transaction, code });
1401
+ onProgress?.({ type: "token_exchange_completed" });
1402
+ return tokens;
1403
+ }
1404
+ async function collectLocalCallbackCode({
1405
+ transaction,
1406
+ timeoutMs,
1407
+ authAction,
1408
+ silent,
1409
+ onProgress
1410
+ }) {
1411
+ const { promise, resolve: resolve4, reject } = getCallablePromise_default();
1412
+ const app = express();
1413
+ app.get("/oauth", (req, res) => {
1043
1414
  res.setHeader("Connection", "close");
1044
- if (req.query.state !== oauthState) {
1045
- rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF"));
1415
+ if (req.query.state !== transaction.state) {
1046
1416
  res.status(400).end("Invalid state. You can close this tab.");
1047
- return;
1048
- }
1049
- if (req.query.error) {
1050
- const desc = req.query.error_description ?? req.query.error;
1051
- rejectCode(new Error(`Authorization denied: ${desc}`));
1417
+ } else if (req.query.error) {
1418
+ reject(
1419
+ new OauthAuthorizationDeniedError(
1420
+ String(req.query.error_description ?? req.query.error)
1421
+ )
1422
+ );
1052
1423
  res.end("Authorization was denied. You can close this tab.");
1053
- return;
1054
- }
1055
- if (!req.query.code) {
1056
- rejectCode(new Error("No authorization code received"));
1424
+ } else if (!req.query.code) {
1425
+ reject(new Error("No authorization code received"));
1057
1426
  res.end("No authorization code received. You can close this tab.");
1058
- return;
1427
+ } else {
1428
+ resolve4(String(req.query.code));
1429
+ res.end("You can now close this tab and return to the CLI.");
1059
1430
  }
1060
- setCode(String(req.query.code));
1061
- res.end("You can now close this tab and return to the CLI.");
1062
1431
  });
1063
- const server = expressApp.listen(availablePort);
1432
+ const server = app.listen(
1433
+ Number(new URL(transaction.redirectUri).port),
1434
+ OAUTH_LOOPBACK_HOST
1435
+ );
1064
1436
  const connections = /* @__PURE__ */ new Set();
1065
1437
  server.on("connection", (conn) => {
1066
1438
  connections.add(conn);
@@ -1068,183 +1440,204 @@ async function runOauthFlow({
1068
1440
  });
1069
1441
  const cleanup = () => {
1070
1442
  server.close();
1071
- log_default.info("\n\u274C Login cancelled by user");
1072
- rejectCode(new ZapierCliUserCancellationError());
1443
+ reject(new ZapierCliUserCancellationError());
1073
1444
  };
1074
1445
  process.on("SIGINT", cleanup);
1075
1446
  process.on("SIGTERM", cleanup);
1076
- const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
1077
- const authUrl = `${authorizeUrl}?${new URLSearchParams({
1078
- response_type: "code",
1079
- client_id: clientId,
1080
- redirect_uri: redirectUri,
1081
- scope,
1082
- state: oauthState,
1083
- code_challenge: codeChallenge,
1084
- code_challenge_method: "S256"
1085
- }).toString()}`;
1086
- log_default.info("Opening your browser to log in.");
1087
- log_default.info("If it doesn't open, visit:", authUrl);
1088
- open(authUrl);
1089
1447
  let timeoutTimer;
1090
1448
  try {
1091
- await spinPromise(
1092
- Promise.race([
1093
- promisedCode,
1094
- new Promise((_resolve, reject) => {
1095
- timeoutTimer = setTimeout(() => {
1096
- reject(
1097
- new Error(
1098
- `Login timed out after ${Math.round(timeoutMs / 1e3)} seconds.`
1099
- )
1100
- );
1101
- }, timeoutMs);
1102
- })
1103
- ]),
1104
- "Waiting for you to login and authorize"
1449
+ await waitForServerListening(server);
1450
+ await openBrowser({ transaction, authAction, silent, onProgress });
1451
+ const waitForCode = Promise.race([
1452
+ promise,
1453
+ new Promise((_resolve, rejectTimeout) => {
1454
+ timeoutTimer = setTimeout(() => {
1455
+ rejectTimeout(new OauthFlowTimeoutError(timeoutMs));
1456
+ }, timeoutMs);
1457
+ })
1458
+ ]);
1459
+ onProgress?.({ type: "callback_waiting" });
1460
+ return silent ? await waitForCode : await spinPromise(
1461
+ waitForCode,
1462
+ `Waiting for you to ${authAction} and authorize`
1105
1463
  );
1106
1464
  } finally {
1107
- if (timeoutTimer) {
1108
- clearTimeout(timeoutTimer);
1109
- }
1465
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1110
1466
  process.off("SIGINT", cleanup);
1111
1467
  process.off("SIGTERM", cleanup);
1112
- await new Promise((resolve4) => {
1113
- const timeout = setTimeout(() => {
1114
- log_default.info("Server close timed out, forcing connection shutdown...");
1115
- connections.forEach((conn) => conn.destroy());
1116
- resolve4();
1117
- }, 1e3);
1118
- server.close(() => {
1119
- clearTimeout(timeout);
1120
- resolve4();
1121
- });
1122
- });
1468
+ await closeServer({ server, connections, silent });
1123
1469
  }
1124
- log_default.info("Exchanging authorization code for tokens...");
1125
- const { data } = await client_default.post(
1126
- tokenUrl,
1127
- {
1128
- grant_type: "authorization_code",
1129
- code: await promisedCode,
1130
- redirect_uri: redirectUri,
1131
- client_id: clientId,
1132
- code_verifier: codeVerifier
1133
- },
1134
- {
1135
- headers: {
1136
- [AUTH_MODE_HEADER]: "no",
1137
- "Content-Type": "application/x-www-form-urlencoded"
1138
- }
1139
- }
1140
- );
1141
- log_default.info("Token exchange completed successfully");
1142
- return {
1143
- accessToken: data.access_token,
1144
- refreshToken: data.refresh_token,
1145
- expiresIn: data.expires_in
1146
- };
1147
1470
  }
1148
-
1149
- // src/utils/auth/client-credentials.ts
1150
- var CREDENTIALS_SCOPES = ["external", "credentials"];
1151
- var EMPTY_POLICY = {
1152
- version: 2,
1153
- statements: []
1154
- };
1155
- async function createCredentialsOnServer(api2, name, policy) {
1156
- const response = await api2.post(
1157
- "/api/v0/client-credentials",
1158
- {
1159
- name,
1160
- allowed_scopes: CREDENTIALS_SCOPES,
1161
- ...policy !== void 0 && { policy }
1162
- },
1163
- { authRequired: true, requiredScopes: ["credentials"] }
1164
- );
1165
- return {
1166
- clientId: response.data.client_id,
1167
- clientSecret: response.data.client_secret
1168
- };
1169
- }
1170
- async function deleteCredentialsOnServer(api2, clientId) {
1171
- await api2.delete(`/api/v0/client-credentials/${clientId}`, void 0, {
1172
- authRequired: true,
1173
- requiredScopes: ["credentials"]
1471
+ async function waitForServerListening(server) {
1472
+ if (server.listening) return;
1473
+ await new Promise((resolve4, reject) => {
1474
+ const cleanup = () => {
1475
+ server.off("listening", handleListening);
1476
+ server.off("error", handleError);
1477
+ };
1478
+ const handleListening = () => {
1479
+ cleanup();
1480
+ resolve4();
1481
+ };
1482
+ const handleError = (error) => {
1483
+ cleanup();
1484
+ reject(error);
1485
+ };
1486
+ server.once("listening", handleListening);
1487
+ server.once("error", handleError);
1174
1488
  });
1175
- }
1176
- async function setupClientCredentials({
1177
- api: api2,
1178
- name,
1179
- credentialsBaseUrl,
1180
- policy
1181
- }) {
1182
- const { clientId, clientSecret } = await createCredentialsOnServer(
1183
- api2,
1184
- name,
1185
- policy
1186
- );
1489
+ }
1490
+ async function openBrowser({
1491
+ transaction,
1492
+ authAction,
1493
+ silent,
1494
+ onProgress
1495
+ }) {
1496
+ if (!silent) {
1497
+ log_default.info(`Opening your browser to ${authAction}.`);
1498
+ log_default.info("If it doesn't open, visit:", transaction.browserAuthUrl);
1499
+ }
1500
+ onProgress?.({ type: "browser_opening", url: transaction.browserAuthUrl });
1187
1501
  try {
1188
- await withRetry({
1189
- action: () => storeClientCredentials({
1190
- name,
1191
- clientId,
1192
- clientSecret,
1193
- scopes: [...CREDENTIALS_SCOPES],
1194
- baseUrl: credentialsBaseUrl
1195
- })
1196
- });
1197
- } catch (storeErr) {
1198
- try {
1199
- await withRetry({
1200
- action: () => deleteCredentialsOnServer(api2, clientId)
1201
- });
1202
- } catch {
1203
- console.error(
1204
- `Failed to roll back orphaned credential ${clientId}. Delete it manually with: zapier-sdk delete-client-credentials ${clientId}`
1502
+ await open(transaction.browserAuthUrl);
1503
+ onProgress?.({ type: "browser_opened", url: transaction.browserAuthUrl });
1504
+ } catch (err) {
1505
+ const reason = err instanceof Error ? err.message : String(err);
1506
+ if (!silent) {
1507
+ log_default.info(
1508
+ `Browser did not open automatically to ${authAction}: ${reason}`
1205
1509
  );
1510
+ log_default.info("Visit this URL manually:", transaction.browserAuthUrl);
1206
1511
  }
1207
- throw storeErr;
1512
+ onProgress?.({
1513
+ type: "browser_open_failed",
1514
+ url: transaction.browserAuthUrl,
1515
+ reason
1516
+ });
1208
1517
  }
1209
- return { clientId };
1210
1518
  }
1211
- function getBaseUrlFromResolvedCredentials(credentials) {
1212
- if (credentials && isCredentialsObject(credentials)) {
1213
- return credentials.baseUrl;
1214
- }
1215
- return void 0;
1519
+ async function closeServer({
1520
+ server,
1521
+ connections,
1522
+ silent
1523
+ }) {
1524
+ await new Promise((resolve4) => {
1525
+ const timeout = setTimeout(() => {
1526
+ if (!silent)
1527
+ log_default.info("Server close timed out, forcing connection shutdown...");
1528
+ connections.forEach((conn) => conn.destroy());
1529
+ resolve4();
1530
+ }, 1e3);
1531
+ server.close(() => {
1532
+ clearTimeout(timeout);
1533
+ resolve4();
1534
+ });
1535
+ });
1216
1536
  }
1217
- function getBaseUrlFromOptionsCredentials(credentials) {
1218
- if (credentials && typeof credentials === "object" && "baseUrl" in credentials && typeof credentials.baseUrl === "string") {
1219
- return credentials.baseUrl;
1220
- }
1221
- return void 0;
1537
+
1538
+ // src/utils/auth/oauth-errors.ts
1539
+ var SENSITIVE_OAUTH_FIELDS = [
1540
+ "access_token",
1541
+ "refresh_token",
1542
+ "id_token",
1543
+ "client_secret",
1544
+ "code_verifier",
1545
+ "code_challenge"
1546
+ ];
1547
+ function getErrorMessage(error) {
1548
+ return error instanceof Error ? error.message : String(error);
1222
1549
  }
1223
- async function resolveCredentialsBaseUrl(context) {
1224
- const resolvedCredentials = "resolvedCredentials" in context ? context.resolvedCredentials : await context.resolveCredentials?.();
1225
- return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
1550
+ function toCamelCase(field) {
1551
+ return field.replace(
1552
+ /_([a-z])/g,
1553
+ (_match, letter) => letter.toUpperCase()
1554
+ );
1226
1555
  }
1227
-
1228
- // src/utils/non-interactive.ts
1229
- function resolveNonInteractive(options) {
1230
- return (options.nonInteractive ?? options.skipPrompts) === true || !process.stdin.isTTY || !process.stdout.isTTY;
1556
+ function escapeRegExp(value) {
1557
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1558
+ }
1559
+ var sensitiveOauthFieldPattern = Array.from(
1560
+ new Set(
1561
+ SENSITIVE_OAUTH_FIELDS.flatMap((field) => [field, toCamelCase(field)])
1562
+ )
1563
+ ).map(escapeRegExp).join("|");
1564
+ var sensitiveQueryParamPattern = new RegExp(
1565
+ `([?&])(${sensitiveOauthFieldPattern})(=)[^&#\\s"'<>]*`,
1566
+ "gi"
1567
+ );
1568
+ function redactSensitiveOauthErrorMessage(message) {
1569
+ return message.replace(
1570
+ sensitiveQueryParamPattern,
1571
+ (_match, prefix, key, separator) => `${prefix}${key}${separator}[REDACTED]`
1572
+ ).replace(
1573
+ new RegExp(`"(${sensitiveOauthFieldPattern})"(\\s*:\\s*)"[^"]*"`, "g"),
1574
+ (_match, key, separator) => `"${key}"${separator}"[REDACTED]"`
1575
+ );
1576
+ }
1577
+ function toRedactedOauthError(error) {
1578
+ const message = redactSensitiveOauthErrorMessage(getErrorMessage(error));
1579
+ if (error instanceof ZapierCliValidationError) {
1580
+ return new ZapierCliValidationError(message);
1581
+ }
1582
+ if (error instanceof Error) {
1583
+ const redactedError = new Error(message);
1584
+ redactedError.name = error.name;
1585
+ return redactedError;
1586
+ }
1587
+ return new ZapierCliValidationError(message);
1231
1588
  }
1232
- var LoginSchema = z.object({
1233
- timeout: z.string().optional().describe("Login timeout in seconds (default: 300)"),
1234
- useApprovals: z.boolean().optional().describe(
1235
- "Require approvals for actions performed with these credentials"
1236
- ),
1237
- nonInteractive: z.boolean().optional().describe(
1238
- "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."
1239
- ),
1240
- /** @deprecated Use `nonInteractive` instead. */
1241
- skipPrompts: z.boolean().optional().meta({
1242
- deprecated: true,
1243
- deprecationMessage: "Use --non-interactive instead."
1244
- })
1245
- }).describe("Log in to Zapier to access your account");
1246
1589
 
1247
- // src/plugins/login/index.ts
1590
+ // src/utils/auth/account-auth.ts
1591
+ 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?";
1592
+ var SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup` to generate a fresh signup URL and try again.";
1593
+ var HEADLESS_SIGNUP_RECOVERY_MESSAGE = "Restart `zapier-sdk signup --headless` to generate a fresh signup URL and try again.";
1594
+ function getEntryPointLabel(entryPoint) {
1595
+ return entryPoint === "signup" ? "Signup" : "Login";
1596
+ }
1597
+ function getActiveCredentialsAction(entryPoint) {
1598
+ return entryPoint === "signup" ? "continue signup" : "log in again";
1599
+ }
1600
+ function getCredentialsPromptMessage(entryPoint) {
1601
+ return entryPoint === "signup" ? "Enter a name to identify these credentials:" : "Enter a name to identify them:";
1602
+ }
1603
+ function getProfileMessage(entryPoint, email) {
1604
+ return entryPoint === "signup" ? `\u{1F464} Authenticated as ${email}` : `\u{1F464} Logged in as ${email}`;
1605
+ }
1606
+ function defaultCredentialsName(email) {
1607
+ return `${email}@${hostname()}`;
1608
+ }
1609
+ function validateCredentialsName(name) {
1610
+ const trimmedName = name.trim();
1611
+ if (!trimmedName) throw new ZapierCliValidationError("Name cannot be empty");
1612
+ return trimmedName;
1613
+ }
1614
+ async function promptCredentialsName({
1615
+ email,
1616
+ promptMessage
1617
+ }) {
1618
+ const { credentialName } = await inquirer.prompt([
1619
+ {
1620
+ type: "input",
1621
+ name: "credentialName",
1622
+ message: promptMessage,
1623
+ default: defaultCredentialsName(email),
1624
+ validate: (input) => {
1625
+ try {
1626
+ validateCredentialsName(input);
1627
+ return true;
1628
+ } catch (err) {
1629
+ return err instanceof Error ? err.message : String(err);
1630
+ }
1631
+ }
1632
+ }
1633
+ ]);
1634
+ return validateCredentialsName(credentialName);
1635
+ }
1636
+ function resolveDefaultCredentialsName({
1637
+ email
1638
+ }) {
1639
+ return validateCredentialsName(defaultCredentialsName(email));
1640
+ }
1248
1641
  function toPkceCredentials(credentials) {
1249
1642
  if (credentials && isCredentialsObject(credentials) && !("clientSecret" in credentials)) {
1250
1643
  return {
@@ -1256,104 +1649,125 @@ function toPkceCredentials(credentials) {
1256
1649
  }
1257
1650
  return void 0;
1258
1651
  }
1259
- async function confirmRevokeAndRelogin(activeCredentials, nonInteractive) {
1260
- if (nonInteractive) {
1652
+ function parseTimeoutSeconds(timeout) {
1653
+ if (timeout === void 0) return 300;
1654
+ const timeoutSeconds = Number(timeout);
1655
+ if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) {
1261
1656
  throw new ZapierCliValidationError(
1262
- `Already logged in as "${activeCredentials.name}". Run \`logout\` first or use an interactive terminal to re-authenticate.`
1657
+ "Timeout must be a positive integer (seconds)."
1263
1658
  );
1264
1659
  }
1660
+ return timeoutSeconds;
1661
+ }
1662
+ async function promptConfirm({
1663
+ message,
1664
+ defaultValue
1665
+ }) {
1265
1666
  const { confirmed } = await inquirer.prompt([
1266
- {
1267
- type: "confirm",
1268
- name: "confirmed",
1667
+ { type: "confirm", name: "confirmed", message, default: defaultValue }
1668
+ ]);
1669
+ return confirmed;
1670
+ }
1671
+ function promptlessCredentialResetError(credentials) {
1672
+ throw new ZapierCliValidationError(
1673
+ `Already logged in as "${credentials.name}". Run \`logout\` first or use an interactive terminal to re-authenticate.`
1674
+ );
1675
+ }
1676
+ function promptlessLegacyJwtUpgradeError() {
1677
+ throw new ZapierCliValidationError(
1678
+ "Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials."
1679
+ );
1680
+ }
1681
+ async function clearExistingAuthState({
1682
+ sdk,
1683
+ baseUrl,
1684
+ interactive,
1685
+ entryPoint
1686
+ }) {
1687
+ const activeCredentials = getActiveCredentials({ baseUrl });
1688
+ const flowLabel = getEntryPointLabel(entryPoint);
1689
+ if (activeCredentials) {
1690
+ const confirmed = interactive ? await promptConfirm({
1691
+ defaultValue: false,
1269
1692
  message: `You are already logged in as "${activeCredentials.name}".
1270
1693
  Logging out will delete these credentials and may interrupt other Zapier SDK or CLI sessions using them.
1271
- Log out and log in again?`,
1272
- default: false
1694
+ Log out and ${getActiveCredentialsAction(entryPoint)}?`
1695
+ }) : promptlessCredentialResetError(activeCredentials);
1696
+ if (!confirmed) {
1697
+ console.log(`${flowLabel} cancelled.`);
1698
+ return false;
1273
1699
  }
1274
- ]);
1275
- if (!confirmed) {
1276
- console.log("Login cancelled.");
1277
- return false;
1278
- }
1279
- return true;
1280
- }
1281
- async function confirmJwtMigration(nonInteractive) {
1282
- if (nonInteractive) {
1283
- throw new ZapierCliValidationError(
1284
- "Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials."
1285
- );
1286
- }
1287
- const { confirmed } = await inquirer.prompt([
1288
- {
1289
- type: "confirm",
1290
- name: "confirmed",
1291
- 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?",
1292
- default: true
1700
+ try {
1701
+ await revokeCredentials({
1702
+ api: sdk.context.api,
1703
+ credentials: activeCredentials
1704
+ });
1705
+ } catch {
1706
+ if (!interactive) {
1707
+ throw new ZapierCliValidationError(
1708
+ `${flowLabel} cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal.`
1709
+ );
1710
+ }
1711
+ const reset = await promptConfirm({
1712
+ defaultValue: false,
1713
+ message: `${flowLabel} cleanup failed. Reset local session state and continue?`
1714
+ });
1715
+ if (!reset) {
1716
+ console.log(`${flowLabel} cancelled.`);
1717
+ return false;
1718
+ }
1719
+ await deleteStoredClientCredentials({
1720
+ name: activeCredentials.name,
1721
+ baseUrl: activeCredentials.baseUrl
1722
+ });
1293
1723
  }
1294
- ]);
1295
- if (!confirmed) {
1296
- console.log("Login cancelled.");
1297
- return false;
1298
- }
1299
- return true;
1300
- }
1301
- async function confirmLocalLoginReset(nonInteractive) {
1302
- if (nonInteractive) {
1303
- throw new ZapierCliValidationError(
1304
- "Login cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal."
1305
- );
1306
- }
1307
- const { confirmed } = await inquirer.prompt([
1308
- {
1309
- type: "confirm",
1310
- name: "confirmed",
1311
- message: "Login cleanup failed. Reset local session state and continue?",
1312
- default: false
1724
+ } else if (hasLegacyJwtConfig()) {
1725
+ const confirmed = interactive ? await promptConfirm({
1726
+ defaultValue: true,
1727
+ message: LEGACY_JWT_UPGRADE_PROMPT
1728
+ }) : promptlessLegacyJwtUpgradeError();
1729
+ if (!confirmed) {
1730
+ console.log(`${flowLabel} cancelled.`);
1731
+ return false;
1313
1732
  }
1314
- ]);
1315
- if (!confirmed) {
1316
- console.log("Login cancelled.");
1317
- return false;
1318
1733
  }
1319
1734
  return true;
1320
1735
  }
1321
- function parseTimeoutSeconds(timeout) {
1322
- const timeoutSeconds = timeout ? parseInt(timeout, 10) : 300;
1323
- if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
1324
- throw new Error("Timeout must be a positive number");
1325
- }
1326
- return timeoutSeconds;
1736
+ async function getProfile(api2) {
1737
+ return api2.get("/zapier/api/v4/profile/", {
1738
+ authRequired: true
1739
+ });
1327
1740
  }
1328
- async function promptCredentialsName(email, nonInteractive) {
1329
- const fallback = `${email}@${hostname()}`;
1330
- if (nonInteractive) {
1331
- return fallback;
1741
+ async function saveClientCredentials({
1742
+ api: api2,
1743
+ name,
1744
+ credentialsBaseUrl,
1745
+ useApprovals,
1746
+ cleanupLogPrefix
1747
+ }) {
1748
+ await setupClientCredentials({
1749
+ api: api2,
1750
+ name,
1751
+ credentialsBaseUrl,
1752
+ ...useApprovals && { policy: EMPTY_POLICY }
1753
+ });
1754
+ try {
1755
+ await clearLegacyJwtState();
1756
+ } catch (err) {
1757
+ console.error(
1758
+ `[${cleanupLogPrefix}] Best-effort legacy JWT cleanup failed:`,
1759
+ err
1760
+ );
1332
1761
  }
1333
- const { credentialName } = await inquirer.prompt([
1334
- {
1335
- type: "input",
1336
- name: "credentialName",
1337
- message: "Enter a name to identify them:",
1338
- default: fallback,
1339
- validate: (input) => {
1340
- if (!input.trim()) return "Name cannot be empty";
1341
- return true;
1342
- }
1343
- }
1344
- ]);
1345
- return credentialName;
1346
1762
  }
1347
- function emitLoginSuccess({
1763
+ function emitAccountAuthSuccess({
1348
1764
  sdk,
1349
1765
  profile
1350
1766
  }) {
1351
1767
  sdk.context.eventEmission.emit(
1352
1768
  "platform.sdk.ApplicationLifecycleEvent",
1353
1769
  buildApplicationLifecycleEvent(
1354
- {
1355
- lifecycle_event_type: "login_success"
1356
- },
1770
+ { lifecycle_event_type: "login_success" },
1357
1771
  {
1358
1772
  customuser_id: profile.user_id,
1359
1773
  account_id: profile.roles[0]?.account_id ?? null
@@ -1361,18 +1775,128 @@ function emitLoginSuccess({
1361
1775
  )
1362
1776
  );
1363
1777
  }
1364
- async function getProfile(api2) {
1365
- return api2.get("/zapier/api/v4/profile/", {
1366
- authRequired: true
1367
- });
1778
+ function emitSignupSuccess({
1779
+ sdk
1780
+ }) {
1781
+ sdk.context.eventEmission.emit(
1782
+ "platform.sdk.ApplicationLifecycleEvent",
1783
+ buildApplicationLifecycleEvent({ lifecycle_event_type: "signup_success" })
1784
+ );
1368
1785
  }
1369
- async function bestEffortClearLegacyJwtState() {
1786
+ async function runOauthWithRedaction(runOauth) {
1370
1787
  try {
1371
- await clearLegacyJwtState();
1372
- } catch (err) {
1373
- console.error("[login] Best-effort legacy JWT cleanup failed:", err);
1788
+ return await runOauth();
1789
+ } catch (error) {
1790
+ if (error instanceof ZapierCliUserCancellationError) throw error;
1791
+ throw toRedactedOauthError(error);
1792
+ }
1793
+ }
1794
+ async function runOauthForEntryPoint({
1795
+ sdk,
1796
+ entryPoint,
1797
+ timeoutMs,
1798
+ pkceCredentials,
1799
+ baseUrl,
1800
+ headless,
1801
+ interactive
1802
+ }) {
1803
+ if (entryPoint === "signup") {
1804
+ return runOauthWithRedaction(
1805
+ () => runSignupOauthFlow({
1806
+ timeoutMs,
1807
+ pkceCredentials,
1808
+ baseUrl,
1809
+ headless,
1810
+ interactive,
1811
+ recoveryMessage: headless ? HEADLESS_SIGNUP_RECOVERY_MESSAGE : SIGNUP_RECOVERY_MESSAGE,
1812
+ onProgress: (event) => {
1813
+ if (event.type === "callback_accepted") {
1814
+ emitSignupSuccess({ sdk });
1815
+ }
1816
+ }
1817
+ })
1818
+ );
1819
+ }
1820
+ return runOauthWithRedaction(
1821
+ () => runLoginOauthFlow({ timeoutMs, pkceCredentials, baseUrl })
1822
+ );
1823
+ }
1824
+ async function runAccountAuth({
1825
+ sdk,
1826
+ options,
1827
+ entryPoint
1828
+ }) {
1829
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
1830
+ const interactive = !resolveNonInteractive(options);
1831
+ const resolvedCredentials = await sdk.context.resolveCredentials();
1832
+ const pkceCredentials = toPkceCredentials(resolvedCredentials);
1833
+ const credentialsBaseUrl = await resolveCredentialsBaseUrl({
1834
+ ...sdk.context,
1835
+ resolvedCredentials
1836
+ });
1837
+ if (!await clearExistingAuthState({
1838
+ sdk,
1839
+ baseUrl: credentialsBaseUrl,
1840
+ interactive,
1841
+ entryPoint
1842
+ })) {
1843
+ return;
1844
+ }
1845
+ const { accessToken } = await runOauthForEntryPoint({
1846
+ sdk,
1847
+ entryPoint,
1848
+ timeoutMs: timeoutSeconds * 1e3,
1849
+ pkceCredentials,
1850
+ baseUrl: credentialsBaseUrl,
1851
+ headless: options.headless === true,
1852
+ interactive
1853
+ });
1854
+ const scopedApi = getOrCreateApiClient({
1855
+ credentials: accessToken,
1856
+ baseUrl: credentialsBaseUrl
1857
+ });
1858
+ const profile = await getProfile(scopedApi);
1859
+ console.log(getProfileMessage(entryPoint, profile.email));
1860
+ console.log(
1861
+ "\nGenerating credentials so this machine can make authenticated requests on your behalf."
1862
+ );
1863
+ const resolveCredentialsName = interactive ? ({ email }) => promptCredentialsName({
1864
+ email,
1865
+ promptMessage: getCredentialsPromptMessage(entryPoint)
1866
+ }) : resolveDefaultCredentialsName;
1867
+ const credentialName = await resolveCredentialsName({ email: profile.email });
1868
+ const useApprovals = options.useApprovals === true;
1869
+ await saveClientCredentials({
1870
+ api: scopedApi,
1871
+ name: credentialName,
1872
+ credentialsBaseUrl,
1873
+ useApprovals,
1874
+ cleanupLogPrefix: entryPoint
1875
+ });
1876
+ console.log(
1877
+ `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
1878
+ );
1879
+ if (useApprovals) {
1880
+ console.log("\u{1F510} Approvals are enabled for these credentials.");
1374
1881
  }
1882
+ emitAccountAuthSuccess({ sdk, profile });
1375
1883
  }
1884
+ var LoginSchema = z.object({
1885
+ timeout: z.string().optional().describe("Login timeout in seconds (default: 300)"),
1886
+ useApprovals: z.boolean().optional().describe(
1887
+ "Require approvals for actions performed with these credentials"
1888
+ ),
1889
+ nonInteractive: z.boolean().optional().describe(
1890
+ "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."
1891
+ ),
1892
+ /** @deprecated Use `nonInteractive` instead. */
1893
+ skipPrompts: z.boolean().optional().meta({
1894
+ deprecated: true,
1895
+ deprecationMessage: "Use --non-interactive instead."
1896
+ })
1897
+ }).describe("Log in to Zapier to access your account");
1898
+
1899
+ // src/plugins/login/index.ts
1376
1900
  var loginPlugin = definePlugin(
1377
1901
  (sdk) => createPluginMethod(sdk, {
1378
1902
  name: "login",
@@ -1380,68 +1904,37 @@ var loginPlugin = definePlugin(
1380
1904
  inputSchema: LoginSchema,
1381
1905
  supportsJsonOutput: false,
1382
1906
  handler: async ({ sdk: sdk2, options }) => {
1383
- const timeoutSeconds = parseTimeoutSeconds(options.timeout);
1384
- const nonInteractive = resolveNonInteractive(options);
1385
- const resolvedCredentials = await sdk2.context.resolveCredentials();
1386
- const pkceCredentials = toPkceCredentials(resolvedCredentials);
1387
- const credentialsBaseUrl = await resolveCredentialsBaseUrl({
1388
- ...sdk2.context,
1389
- resolvedCredentials
1390
- });
1391
- const activeCredentials = getActiveCredentials({
1392
- baseUrl: credentialsBaseUrl
1393
- });
1394
- if (activeCredentials) {
1395
- if (!await confirmRevokeAndRelogin(activeCredentials, nonInteractive))
1396
- return;
1397
- try {
1398
- await revokeCredentials({
1399
- api: sdk2.context.api,
1400
- credentials: activeCredentials
1401
- });
1402
- } catch {
1403
- if (!await confirmLocalLoginReset(nonInteractive)) return;
1404
- await deleteStoredClientCredentials({
1405
- name: activeCredentials.name,
1406
- baseUrl: activeCredentials.baseUrl
1407
- });
1408
- }
1409
- } else if (hasLegacyJwtConfig()) {
1410
- if (!await confirmJwtMigration(nonInteractive)) return;
1411
- }
1412
- const { accessToken } = await runOauthFlow({
1413
- timeoutMs: timeoutSeconds * 1e3,
1414
- pkceCredentials,
1415
- baseUrl: credentialsBaseUrl
1416
- });
1417
- const scopedApi = getOrCreateApiClient({
1418
- credentials: accessToken,
1419
- baseUrl: credentialsBaseUrl
1420
- });
1421
- const profile = await getProfile(scopedApi);
1422
- console.log(`\u{1F464} Logged in as ${profile.email}`);
1423
- console.log(
1424
- "\nGenerating credentials so this machine can make authenticated requests on your behalf."
1425
- );
1426
- const credentialName = await promptCredentialsName(
1427
- profile.email,
1428
- nonInteractive
1429
- );
1430
- const useApprovals = options.useApprovals === true;
1431
- await setupClientCredentials({
1432
- api: scopedApi,
1433
- name: credentialName,
1434
- credentialsBaseUrl,
1435
- ...useApprovals && { policy: EMPTY_POLICY }
1436
- });
1437
- await bestEffortClearLegacyJwtState();
1438
- console.log(
1439
- `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
1440
- );
1441
- if (useApprovals) {
1442
- console.log("\u{1F510} Approvals are enabled for these credentials.");
1443
- }
1444
- emitLoginSuccess({ sdk: sdk2, profile });
1907
+ await runAccountAuth({ sdk: sdk2, options, entryPoint: "login" });
1908
+ }
1909
+ })
1910
+ );
1911
+ var SignupSchema = z.object({
1912
+ timeout: z.string().optional().describe("Signup timeout in seconds (default: 300)"),
1913
+ useApprovals: z.boolean().optional().describe(
1914
+ "Require approvals for actions performed with these credentials"
1915
+ ),
1916
+ nonInteractive: z.boolean().optional().describe(
1917
+ "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."
1918
+ ),
1919
+ /** @deprecated Use `nonInteractive` instead. */
1920
+ skipPrompts: z.boolean().optional().meta({
1921
+ deprecated: true,
1922
+ deprecationMessage: "Use --non-interactive instead."
1923
+ }),
1924
+ headless: z.boolean().optional().describe(
1925
+ "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."
1926
+ )
1927
+ }).describe("Set up Zapier account access and SDK credentials");
1928
+
1929
+ // src/plugins/signup/index.ts
1930
+ var signupPlugin = definePlugin(
1931
+ (sdk) => createPluginMethod(sdk, {
1932
+ name: "signup",
1933
+ categories: ["account"],
1934
+ inputSchema: SignupSchema,
1935
+ supportsJsonOutput: false,
1936
+ handler: async ({ sdk: sdk2, options }) => {
1937
+ await runAccountAuth({ sdk: sdk2, options, entryPoint: "signup" });
1445
1938
  }
1446
1939
  })
1447
1940
  );
@@ -1508,7 +2001,8 @@ var BundleCodeSchema = z.object({
1508
2001
  var bundleCodePlugin = definePlugin(
1509
2002
  (sdk) => createPluginMethod(sdk, {
1510
2003
  name: "bundleCode",
1511
- categories: ["utility", "deprecated"],
2004
+ categories: ["utility"],
2005
+ deprecation: { message: "bundleCode is no longer maintained." },
1512
2006
  inputSchema: BundleCodeSchema,
1513
2007
  handler: async ({ options }) => bundleCode(options)
1514
2008
  })
@@ -1604,7 +2098,7 @@ async function detectTypesOutputDirectory() {
1604
2098
  }
1605
2099
  return "./zapier/apps/";
1606
2100
  }
1607
- var addPlugin = definePlugin(
2101
+ var addAppsPlugin = definePlugin(
1608
2102
  (sdk) => createPluginMethod(sdk, {
1609
2103
  name: "add",
1610
2104
  categories: ["utility"],
@@ -3009,10 +3503,6 @@ var cliOverridesPlugin = definePlugin(
3009
3503
  if (sdk.context.meta.fetch) {
3010
3504
  meta.fetch = {
3011
3505
  ...sdk.context.meta.fetch,
3012
- categories: [
3013
- ...sdk.context.meta.fetch.categories || [],
3014
- "deprecated"
3015
- ],
3016
3506
  deprecation: {
3017
3507
  message: "This command is deprecated and will be removed soon. Use `curl` instead. Learn more: https://docs.zapier.com/sdk/cli-reference#curl"
3018
3508
  }
@@ -3976,7 +4466,7 @@ definePlugin(
3976
4466
  // package.json with { type: 'json' }
3977
4467
  var package_default = {
3978
4468
  name: "@zapier/zapier-sdk-cli",
3979
- version: "0.52.10"};
4469
+ version: "0.53.0"};
3980
4470
 
3981
4471
  // src/sdk.ts
3982
4472
  injectCliLogin(login_exports);
@@ -3985,26 +4475,26 @@ function createZapierCliSdk(options = {}) {
3985
4475
  const extensionsContextPlugin = () => ({
3986
4476
  context: { extensions }
3987
4477
  });
3988
- let chain = createZapierSdk({
4478
+ const sdk = createZapierSdkStack({
3989
4479
  ...sdkOptions,
3990
4480
  eventEmission: { ...sdkOptions.eventEmission, callContext: "cli" },
3991
4481
  callerPackage: { name: package_default.name, version: package_default.version }
3992
- }).addPlugin(extensionsContextPlugin).addPlugin(generateAppTypesPlugin).addPlugin(buildManifestPlugin).addPlugin(bundleCodePlugin).addPlugin(getLoginConfigPathPlugin).addPlugin(addPlugin).addPlugin(feedbackPlugin).addPlugin(curlPlugin).addPlugin(initPlugin).addPlugin(mcpPlugin).addPlugin(loginPlugin).addPlugin(logoutPlugin).addPlugin(cliOverridesPlugin);
4482
+ }).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();
3993
4483
  for (const ext of extensions) {
3994
4484
  try {
3995
- chain = chain.addPlugin(ext);
4485
+ addPlugin(sdk, ext);
3996
4486
  } catch (err) {
3997
4487
  console.warn(
3998
4488
  `Extension plugin failed to construct: ${err.message}; skipping.`
3999
4489
  );
4000
4490
  }
4001
4491
  }
4002
- return chain;
4492
+ return sdk;
4003
4493
  }
4004
4494
 
4005
4495
  // package.json
4006
4496
  var package_default2 = {
4007
- version: "0.52.10"};
4497
+ version: "0.53.0"};
4008
4498
 
4009
4499
  // src/telemetry/builders.ts
4010
4500
  function createCliBaseEvent(context = {}) {