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