@zapier/zapier-sdk-cli 0.52.12 → 0.53.1

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