@zapier/zapier-sdk-cli 0.52.12 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +20 -0
  3. package/dist/cli.cjs +896 -395
  4. package/dist/cli.mjs +897 -396
  5. package/dist/experimental.cjs +897 -398
  6. package/dist/experimental.d.mts +1 -1
  7. package/dist/experimental.d.ts +1 -1
  8. package/dist/experimental.mjs +896 -397
  9. package/dist/index.cjs +898 -399
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.mjs +897 -398
  13. package/dist/package.json +1 -1
  14. package/dist/{sdk-Sa1HjzUj.d.mts → sdk-SOLizjno.d.mts} +40 -2
  15. package/dist/{sdk-Sa1HjzUj.d.ts → sdk-SOLizjno.d.ts} +40 -2
  16. package/dist/src/experimental.js +2 -1
  17. package/dist/src/plugins/index.d.ts +1 -0
  18. package/dist/src/plugins/index.js +1 -0
  19. package/dist/src/plugins/login/index.d.ts +2 -15
  20. package/dist/src/plugins/login/index.js +3 -191
  21. package/dist/src/plugins/signup/index.d.ts +25 -0
  22. package/dist/src/plugins/signup/index.js +12 -0
  23. package/dist/src/plugins/signup/schemas.d.ts +9 -0
  24. package/dist/src/plugins/signup/schemas.js +26 -0
  25. package/dist/src/plugins/signup/test-harness.d.ts +34 -0
  26. package/dist/src/plugins/signup/test-harness.js +74 -0
  27. package/dist/src/sdk.js +2 -1
  28. package/dist/src/types/sdk.d.ts +2 -1
  29. package/dist/src/utils/auth/account-auth.d.ts +32 -0
  30. package/dist/src/utils/auth/account-auth.js +265 -0
  31. package/dist/src/utils/auth/oauth-callback.d.ts +6 -0
  32. package/dist/src/utils/auth/oauth-callback.js +28 -0
  33. package/dist/src/utils/auth/oauth-errors.d.ts +2 -0
  34. package/dist/src/utils/auth/oauth-errors.js +39 -0
  35. package/dist/src/utils/auth/oauth-flow.d.ts +31 -6
  36. package/dist/src/utils/auth/oauth-flow.js +258 -106
  37. package/dist/src/utils/auth/oauth-transaction.d.ts +35 -0
  38. package/dist/src/utils/auth/oauth-transaction.js +69 -0
  39. package/dist/src/utils/non-interactive.d.ts +5 -4
  40. package/dist/src/utils/non-interactive.js +6 -5
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +3 -3
@@ -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, getOrCreateApiClient, OutputPropertySchema, ZapierBundleError, DEFAULT_CONFIG_PATH, ZapierValidationError, ZapierUnknownError, ZapierReleaseTriggerMessageSignal, isCredentialsObject, invalidateCachedToken, buildApplicationLifecycleEvent, batch, toSnakeCase, ZapierAbortDrainSignal, ZapierAuthenticationError, ZapierError } from '@zapier/zapier-sdk';
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';
11
11
  import { z } from 'zod';
12
12
  import { injectCliLogin, createZapierSdkStack, addPlugin } from '@zapier/zapier-sdk/experimental';
13
13
  import { hostname } from 'os';
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';
@@ -852,10 +853,27 @@ async function revokeCredentials({
852
853
  });
853
854
  emitAuthLogout(onEvent);
854
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
+ }
855
872
 
856
- // src/utils/constants.ts
857
- var LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
858
- 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
+ }
859
877
  var ZapierCliError = class extends ZapierError {
860
878
  };
861
879
  var ZapierCliUserCancellationError = class extends ZapierCliError {
@@ -883,22 +901,90 @@ var ZapierCliValidationError = class extends ZapierCliError {
883
901
  }
884
902
  };
885
903
 
886
- // src/utils/spinner.ts
887
- var spinPromise = async (promise, text) => {
888
- 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
+ );
889
942
  try {
890
- const result = await promise;
891
- spinner.succeed();
892
- return result;
893
- } catch (error) {
894
- if (error instanceof ZapierCliUserCancellationError) {
895
- spinner.stop();
896
- } else {
897
- 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
+ );
898
961
  }
899
- throw error;
962
+ throw storeErr;
900
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
+ };
901
986
  };
987
+ var getCallablePromise_default = getCallablePromise;
902
988
  var log = {
903
989
  info: (message, ...args) => {
904
990
  console.error(chalk3.blue("\u2139"), message, ...args);
@@ -919,6 +1005,60 @@ var log = {
919
1005
  }
920
1006
  };
921
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
+ }
922
1062
 
923
1063
  // src/utils/api/client.ts
924
1064
  var createApiClient = () => {
@@ -949,179 +1089,77 @@ var createApiClient = () => {
949
1089
  var api = createApiClient();
950
1090
  var client_default = api;
951
1091
 
952
- // src/utils/getCallablePromise.ts
953
- var getCallablePromise = () => {
954
- let resolve4 = () => {
955
- };
956
- let reject = () => {
957
- };
958
- const promise = new Promise((_resolve, _reject) => {
959
- resolve4 = _resolve;
960
- reject = _reject;
961
- });
962
- return {
963
- promise,
964
- resolve: resolve4,
965
- reject
966
- };
967
- };
968
- var getCallablePromise_default = getCallablePromise;
969
-
970
- // src/utils/auth/oauth-flow.ts
971
- var findAvailablePort = () => {
972
- return new Promise((resolve4, reject) => {
973
- let portIndex = 0;
974
- const tryPort = (port) => {
975
- const server = express().listen(port, () => {
976
- server.close();
977
- resolve4(port);
978
- });
979
- server.on("error", (err) => {
980
- if (err.code === "EADDRINUSE") {
981
- if (portIndex < LOGIN_PORTS.length) {
982
- tryPort(LOGIN_PORTS[portIndex++]);
983
- } else {
984
- reject(
985
- new Error(
986
- `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`
987
- )
988
- );
989
- }
990
- } else {
991
- reject(err);
992
- }
993
- });
994
- };
995
- if (LOGIN_PORTS.length > 0) {
996
- tryPort(LOGIN_PORTS[portIndex++]);
997
- } else {
998
- reject(new Error("No OAuth callback ports configured"));
999
- }
1000
- });
1001
- };
1002
- var generateRandomString = () => {
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() {
1003
1109
  const array = new Uint32Array(28);
1004
1110
  crypto.getRandomValues(array);
1005
1111
  return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join(
1006
1112
  ""
1007
1113
  );
1008
- };
1114
+ }
1009
1115
  function ensureOfflineAccess(scope) {
1010
- if (scope.includes("offline_access")) {
1011
- return scope;
1012
- }
1116
+ if (scope.includes("offline_access")) return scope;
1013
1117
  return `${scope} offline_access`;
1014
1118
  }
1015
- async function runOauthFlow({
1016
- timeoutMs = LOGIN_TIMEOUT_MS,
1119
+ async function prepareOauthTransaction({
1017
1120
  pkceCredentials,
1018
- baseUrl
1121
+ baseUrl,
1122
+ redirectUri,
1123
+ entryPoint = "login"
1019
1124
  }) {
1020
1125
  const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
1021
1126
  credentials: pkceCredentials,
1022
1127
  baseUrl
1023
1128
  });
1024
- const scope = ensureOfflineAccess(
1025
- pkceCredentials?.scope || "internal credentials"
1026
- );
1027
- const availablePort = await findAvailablePort();
1028
- const redirectUri = `http://localhost:${availablePort}/oauth`;
1029
- log_default.info(`Using port ${availablePort} for OAuth callback`);
1030
- const {
1031
- promise: promisedCode,
1032
- resolve: setCode,
1033
- reject: rejectCode
1034
- } = getCallablePromise_default();
1035
- const oauthState = generateRandomString();
1036
- const expressApp = express();
1037
- expressApp.get("/oauth", (req, res) => {
1038
- res.setHeader("Connection", "close");
1039
- if (req.query.state !== oauthState) {
1040
- rejectCode(new Error("OAuth state mismatch \u2014 possible CSRF"));
1041
- res.status(400).end("Invalid state. You can close this tab.");
1042
- return;
1043
- }
1044
- if (req.query.error) {
1045
- const desc = req.query.error_description ?? req.query.error;
1046
- rejectCode(new Error(`Authorization denied: ${desc}`));
1047
- res.end("Authorization was denied. You can close this tab.");
1048
- return;
1049
- }
1050
- if (!req.query.code) {
1051
- rejectCode(new Error("No authorization code received"));
1052
- res.end("No authorization code received. You can close this tab.");
1053
- return;
1054
- }
1055
- setCode(String(req.query.code));
1056
- res.end("You can now close this tab and return to the CLI.");
1057
- });
1058
- const server = expressApp.listen(availablePort);
1059
- const connections = /* @__PURE__ */ new Set();
1060
- server.on("connection", (conn) => {
1061
- connections.add(conn);
1062
- conn.on("close", () => connections.delete(conn));
1063
- });
1064
- const cleanup = () => {
1065
- server.close();
1066
- log_default.info("\n\u274C Login cancelled by user");
1067
- rejectCode(new ZapierCliUserCancellationError());
1068
- };
1069
- process.on("SIGINT", cleanup);
1070
- process.on("SIGTERM", cleanup);
1071
1129
  const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
1130
+ const state = generateRandomString();
1072
1131
  const authUrl = `${authorizeUrl}?${new URLSearchParams({
1073
1132
  response_type: "code",
1074
1133
  client_id: clientId,
1075
1134
  redirect_uri: redirectUri,
1076
- scope,
1077
- state: oauthState,
1135
+ scope: ensureOfflineAccess(
1136
+ pkceCredentials?.scope || "internal credentials"
1137
+ ),
1138
+ state,
1078
1139
  code_challenge: codeChallenge,
1079
1140
  code_challenge_method: "S256"
1080
1141
  }).toString()}`;
1081
- log_default.info("Opening your browser to log in.");
1082
- log_default.info("If it doesn't open, visit:", authUrl);
1083
- open(authUrl);
1084
- let timeoutTimer;
1085
- try {
1086
- await spinPromise(
1087
- Promise.race([
1088
- promisedCode,
1089
- new Promise((_resolve, reject) => {
1090
- timeoutTimer = setTimeout(() => {
1091
- reject(
1092
- new Error(
1093
- `Login timed out after ${Math.round(timeoutMs / 1e3)} seconds.`
1094
- )
1095
- );
1096
- }, timeoutMs);
1097
- })
1098
- ]),
1099
- "Waiting for you to login and authorize"
1100
- );
1101
- } finally {
1102
- if (timeoutTimer) {
1103
- clearTimeout(timeoutTimer);
1104
- }
1105
- process.off("SIGINT", cleanup);
1106
- process.off("SIGTERM", cleanup);
1107
- await new Promise((resolve4) => {
1108
- const timeout = setTimeout(() => {
1109
- log_default.info("Server close timed out, forcing connection shutdown...");
1110
- connections.forEach((conn) => conn.destroy());
1111
- resolve4();
1112
- }, 1e3);
1113
- server.close(() => {
1114
- clearTimeout(timeout);
1115
- resolve4();
1116
- });
1117
- });
1118
- }
1119
- log_default.info("Exchanging authorization code for tokens...");
1142
+ return {
1143
+ browserAuthUrl: buildBrowserAuthUrl({ authorizeUrl: authUrl, entryPoint }),
1144
+ clientId,
1145
+ codeVerifier,
1146
+ redirectUri,
1147
+ state,
1148
+ tokenUrl
1149
+ };
1150
+ }
1151
+ async function exchangeOauthCode({
1152
+ tokenUrl,
1153
+ code,
1154
+ redirectUri,
1155
+ clientId,
1156
+ codeVerifier
1157
+ }) {
1120
1158
  const { data } = await client_default.post(
1121
1159
  tokenUrl,
1122
1160
  {
1123
1161
  grant_type: "authorization_code",
1124
- code: await promisedCode,
1162
+ code,
1125
1163
  redirect_uri: redirectUri,
1126
1164
  client_id: clientId,
1127
1165
  code_verifier: codeVerifier
@@ -1133,7 +1171,6 @@ async function runOauthFlow({
1133
1171
  }
1134
1172
  }
1135
1173
  );
1136
- log_default.info("Token exchange completed successfully");
1137
1174
  return {
1138
1175
  accessToken: data.access_token,
1139
1176
  refreshToken: data.refresh_token,
@@ -1141,105 +1178,467 @@ async function runOauthFlow({
1141
1178
  };
1142
1179
  }
1143
1180
 
1144
- // src/utils/auth/client-credentials.ts
1145
- var CREDENTIALS_SCOPES = ["external", "credentials"];
1146
- var EMPTY_POLICY = {
1147
- version: 2,
1148
- statements: []
1181
+ // src/utils/auth/oauth-flow.ts
1182
+ var OauthFlowTimeoutError = class extends Error {
1183
+ constructor(timeoutMs) {
1184
+ super("OAuth flow timed out");
1185
+ this.timeoutMs = timeoutMs;
1186
+ this.name = "OauthFlowTimeoutError";
1187
+ }
1149
1188
  };
1150
- async function createCredentialsOnServer(api2, name, policy) {
1151
- const response = await api2.post(
1152
- "/api/v0/client-credentials",
1153
- {
1154
- name,
1155
- allowed_scopes: CREDENTIALS_SCOPES,
1156
- ...policy !== void 0 && { policy }
1157
- },
1158
- { authRequired: true, requiredScopes: ["credentials"] }
1159
- );
1160
- return {
1161
- clientId: response.data.client_id,
1162
- clientSecret: response.data.client_secret
1163
- };
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() {
1197
+ return new Promise((resolve4, reject) => {
1198
+ let portIndex = 0;
1199
+ const tryPort = (port) => {
1200
+ const server = express().listen(port, OAUTH_LOOPBACK_HOST, () => {
1201
+ server.close();
1202
+ resolve4(port);
1203
+ });
1204
+ server.on("error", (err) => {
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
+ );
1213
+ } else {
1214
+ reject(err);
1215
+ }
1216
+ });
1217
+ };
1218
+ if (LOGIN_PORTS.length > 0) tryPort(LOGIN_PORTS[portIndex++]);
1219
+ else reject(new Error("No OAuth callback ports configured"));
1220
+ });
1164
1221
  }
1165
- async function deleteCredentialsOnServer(api2, clientId) {
1166
- await api2.delete(`/api/v0/client-credentials/${clientId}`, void 0, {
1167
- authRequired: true,
1168
- requiredScopes: ["credentials"]
1222
+ async function runLoginOauthFlow(options) {
1223
+ return runOauthFlowEntryPoint({
1224
+ ...options,
1225
+ entryPoint: "login",
1226
+ authAction: "log in",
1227
+ flowName: "Login"
1169
1228
  });
1170
1229
  }
1171
- async function setupClientCredentials({
1172
- api: api2,
1173
- name,
1174
- credentialsBaseUrl,
1175
- policy
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
1176
1250
  }) {
1177
- const { clientId, clientSecret } = await createCredentialsOnServer(
1178
- api2,
1179
- name,
1180
- policy
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`);
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
1296
+ });
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
1181
1320
  );
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
+ });
1182
1353
  try {
1183
- await withRetry({
1184
- action: () => storeClientCredentials({
1185
- name,
1186
- clientId,
1187
- clientSecret,
1188
- scopes: [...CREDENTIALS_SCOPES],
1189
- baseUrl: credentialsBaseUrl
1190
- })
1354
+ return await readUrl.catch((error) => {
1355
+ if (error instanceof Error && error.name === "AbortError") {
1356
+ throw new Error(timeoutMessage);
1357
+ }
1358
+ throw error;
1191
1359
  });
1192
- } catch (storeErr) {
1193
- try {
1194
- await withRetry({
1195
- action: () => deleteCredentialsOnServer(api2, clientId)
1196
- });
1197
- } catch {
1198
- console.error(
1199
- `Failed to roll back orphaned credential ${clientId}. Delete it manually with: zapier-sdk delete-client-credentials ${clientId}`
1360
+ } finally {
1361
+ clearTimeout(timeoutTimer);
1362
+ rl.close();
1363
+ }
1364
+ }
1365
+ async function runHeadlessSignupOauthFlow({
1366
+ timeoutMs = LOGIN_TIMEOUT_MS,
1367
+ pkceCredentials,
1368
+ baseUrl,
1369
+ interactive = true,
1370
+ onProgress,
1371
+ recoveryMessage
1372
+ }) {
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"
1379
+ });
1380
+ console.log(
1381
+ "Use this mode when signing up from a machine that has no browser."
1382
+ );
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) => {
1415
+ res.setHeader("Connection", "close");
1416
+ if (req.query.state !== transaction.state) {
1417
+ res.status(400).end("Invalid state. You can close this tab.");
1418
+ } else if (req.query.error) {
1419
+ reject(
1420
+ new OauthAuthorizationDeniedError(
1421
+ String(req.query.error_description ?? req.query.error)
1422
+ )
1200
1423
  );
1424
+ res.end("Authorization was denied. You can close this tab.");
1425
+ } else if (!req.query.code) {
1426
+ reject(new Error("No authorization code received"));
1427
+ res.end("No authorization code received. You can close this tab.");
1428
+ } else {
1429
+ resolve4(String(req.query.code));
1430
+ res.end("You can now close this tab and return to the CLI.");
1201
1431
  }
1202
- throw storeErr;
1432
+ });
1433
+ const server = app.listen(
1434
+ Number(new URL(transaction.redirectUri).port),
1435
+ OAUTH_LOOPBACK_HOST
1436
+ );
1437
+ const connections = /* @__PURE__ */ new Set();
1438
+ server.on("connection", (conn) => {
1439
+ connections.add(conn);
1440
+ conn.on("close", () => connections.delete(conn));
1441
+ });
1442
+ const cleanup = () => {
1443
+ server.close();
1444
+ reject(new ZapierCliUserCancellationError());
1445
+ };
1446
+ process.on("SIGINT", cleanup);
1447
+ process.on("SIGTERM", cleanup);
1448
+ let timeoutTimer;
1449
+ try {
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`
1464
+ );
1465
+ } finally {
1466
+ if (timeoutTimer) clearTimeout(timeoutTimer);
1467
+ process.off("SIGINT", cleanup);
1468
+ process.off("SIGTERM", cleanup);
1469
+ await closeServer({ server, connections, silent });
1203
1470
  }
1204
- return { clientId };
1205
1471
  }
1206
- function getBaseUrlFromResolvedCredentials(credentials) {
1207
- if (credentials && isCredentialsObject(credentials)) {
1208
- return credentials.baseUrl;
1209
- }
1210
- return void 0;
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);
1489
+ });
1211
1490
  }
1212
- function getBaseUrlFromOptionsCredentials(credentials) {
1213
- if (credentials && typeof credentials === "object" && "baseUrl" in credentials && typeof credentials.baseUrl === "string") {
1214
- return credentials.baseUrl;
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 });
1502
+ try {
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}`
1510
+ );
1511
+ log_default.info("Visit this URL manually:", transaction.browserAuthUrl);
1512
+ }
1513
+ onProgress?.({
1514
+ type: "browser_open_failed",
1515
+ url: transaction.browserAuthUrl,
1516
+ reason
1517
+ });
1215
1518
  }
1216
- return void 0;
1217
1519
  }
1218
- async function resolveCredentialsBaseUrl(context) {
1219
- const resolvedCredentials = "resolvedCredentials" in context ? context.resolvedCredentials : await context.resolveCredentials?.();
1220
- return getBaseUrlFromResolvedCredentials(resolvedCredentials) ?? getBaseUrlFromOptionsCredentials(context.options?.credentials) ?? context.options?.baseUrl;
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
+ });
1221
1537
  }
1222
1538
 
1223
- // src/utils/non-interactive.ts
1224
- function resolveNonInteractive(options) {
1225
- return (options.nonInteractive ?? options.skipPrompts) === true || !process.stdin.isTTY || !process.stdout.isTTY;
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);
1550
+ }
1551
+ function toCamelCase(field) {
1552
+ return field.replace(
1553
+ /_([a-z])/g,
1554
+ (_match, letter) => letter.toUpperCase()
1555
+ );
1556
+ }
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);
1226
1589
  }
1227
- var LoginSchema = z.object({
1228
- timeout: z.string().optional().describe("Login timeout in seconds (default: 300)"),
1229
- useApprovals: z.boolean().optional().describe(
1230
- "Require approvals for actions performed with these credentials"
1231
- ),
1232
- nonInteractive: z.boolean().optional().describe(
1233
- "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."
1234
- ),
1235
- /** @deprecated Use `nonInteractive` instead. */
1236
- skipPrompts: z.boolean().optional().meta({
1237
- deprecated: true,
1238
- deprecationMessage: "Use --non-interactive instead."
1239
- })
1240
- }).describe("Log in to Zapier to access your account");
1241
1590
 
1242
- // 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
+ }
1243
1642
  function toPkceCredentials(credentials) {
1244
1643
  if (credentials && isCredentialsObject(credentials) && !("clientSecret" in credentials)) {
1245
1644
  return {
@@ -1251,104 +1650,125 @@ function toPkceCredentials(credentials) {
1251
1650
  }
1252
1651
  return void 0;
1253
1652
  }
1254
- async function confirmRevokeAndRelogin(activeCredentials, nonInteractive) {
1255
- 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) {
1256
1657
  throw new ZapierCliValidationError(
1257
- `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)."
1258
1659
  );
1259
1660
  }
1661
+ return timeoutSeconds;
1662
+ }
1663
+ async function promptConfirm({
1664
+ message,
1665
+ defaultValue
1666
+ }) {
1260
1667
  const { confirmed } = await inquirer.prompt([
1261
- {
1262
- type: "confirm",
1263
- 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,
1264
1693
  message: `You are already logged in as "${activeCredentials.name}".
1265
1694
  Logging out will delete these credentials and may interrupt other Zapier SDK or CLI sessions using them.
1266
- Log out and log in again?`,
1267
- default: false
1695
+ Log out and ${getActiveCredentialsAction(entryPoint)}?`
1696
+ }) : promptlessCredentialResetError(activeCredentials);
1697
+ if (!confirmed) {
1698
+ console.log(`${flowLabel} cancelled.`);
1699
+ return false;
1268
1700
  }
1269
- ]);
1270
- if (!confirmed) {
1271
- console.log("Login cancelled.");
1272
- return false;
1273
- }
1274
- return true;
1275
- }
1276
- async function confirmJwtMigration(nonInteractive) {
1277
- if (nonInteractive) {
1278
- throw new ZapierCliValidationError(
1279
- "Legacy JWT login detected. Run `logout` first or use an interactive terminal to migrate to client credentials."
1280
- );
1281
- }
1282
- const { confirmed } = await inquirer.prompt([
1283
- {
1284
- type: "confirm",
1285
- name: "confirmed",
1286
- 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?",
1287
- 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
+ });
1288
1724
  }
1289
- ]);
1290
- if (!confirmed) {
1291
- console.log("Login cancelled.");
1292
- return false;
1293
- }
1294
- return true;
1295
- }
1296
- async function confirmLocalLoginReset(nonInteractive) {
1297
- if (nonInteractive) {
1298
- throw new ZapierCliValidationError(
1299
- "Login cleanup failed and cannot be reset without confirmation. Re-run with an interactive terminal."
1300
- );
1301
- }
1302
- const { confirmed } = await inquirer.prompt([
1303
- {
1304
- type: "confirm",
1305
- name: "confirmed",
1306
- message: "Login cleanup failed. Reset local session state and continue?",
1307
- 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;
1308
1733
  }
1309
- ]);
1310
- if (!confirmed) {
1311
- console.log("Login cancelled.");
1312
- return false;
1313
1734
  }
1314
1735
  return true;
1315
1736
  }
1316
- function parseTimeoutSeconds(timeout) {
1317
- const timeoutSeconds = timeout ? parseInt(timeout, 10) : 300;
1318
- if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
1319
- throw new Error("Timeout must be a positive number");
1320
- }
1321
- return timeoutSeconds;
1737
+ async function getProfile(api2) {
1738
+ return api2.get("/zapier/api/v4/profile/", {
1739
+ authRequired: true
1740
+ });
1322
1741
  }
1323
- async function promptCredentialsName(email, nonInteractive) {
1324
- const fallback = `${email}@${hostname()}`;
1325
- if (nonInteractive) {
1326
- 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
+ );
1327
1762
  }
1328
- const { credentialName } = await inquirer.prompt([
1329
- {
1330
- type: "input",
1331
- name: "credentialName",
1332
- message: "Enter a name to identify them:",
1333
- default: fallback,
1334
- validate: (input) => {
1335
- if (!input.trim()) return "Name cannot be empty";
1336
- return true;
1337
- }
1338
- }
1339
- ]);
1340
- return credentialName;
1341
1763
  }
1342
- function emitLoginSuccess({
1764
+ function emitAccountAuthSuccess({
1343
1765
  sdk,
1344
1766
  profile
1345
1767
  }) {
1346
1768
  sdk.context.eventEmission.emit(
1347
1769
  "platform.sdk.ApplicationLifecycleEvent",
1348
1770
  buildApplicationLifecycleEvent(
1349
- {
1350
- lifecycle_event_type: "login_success"
1351
- },
1771
+ { lifecycle_event_type: "login_success" },
1352
1772
  {
1353
1773
  customuser_id: profile.user_id,
1354
1774
  account_id: profile.roles[0]?.account_id ?? null
@@ -1356,18 +1776,128 @@ function emitLoginSuccess({
1356
1776
  )
1357
1777
  );
1358
1778
  }
1359
- async function getProfile(api2) {
1360
- return api2.get("/zapier/api/v4/profile/", {
1361
- authRequired: true
1362
- });
1779
+ function emitSignupSuccess({
1780
+ sdk
1781
+ }) {
1782
+ sdk.context.eventEmission.emit(
1783
+ "platform.sdk.ApplicationLifecycleEvent",
1784
+ buildApplicationLifecycleEvent({ lifecycle_event_type: "signup_success" })
1785
+ );
1363
1786
  }
1364
- async function bestEffortClearLegacyJwtState() {
1787
+ async function runOauthWithRedaction(runOauth) {
1365
1788
  try {
1366
- await clearLegacyJwtState();
1367
- } catch (err) {
1368
- 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.");
1369
1882
  }
1883
+ emitAccountAuthSuccess({ sdk, profile });
1370
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
1371
1901
  var loginPlugin = definePlugin(
1372
1902
  (sdk) => createPluginMethod(sdk, {
1373
1903
  name: "login",
@@ -1375,68 +1905,37 @@ var loginPlugin = definePlugin(
1375
1905
  inputSchema: LoginSchema,
1376
1906
  supportsJsonOutput: false,
1377
1907
  handler: async ({ sdk: sdk2, options }) => {
1378
- const timeoutSeconds = parseTimeoutSeconds(options.timeout);
1379
- const nonInteractive = resolveNonInteractive(options);
1380
- const resolvedCredentials = await sdk2.context.resolveCredentials();
1381
- const pkceCredentials = toPkceCredentials(resolvedCredentials);
1382
- const credentialsBaseUrl = await resolveCredentialsBaseUrl({
1383
- ...sdk2.context,
1384
- resolvedCredentials
1385
- });
1386
- const activeCredentials = getActiveCredentials({
1387
- baseUrl: credentialsBaseUrl
1388
- });
1389
- if (activeCredentials) {
1390
- if (!await confirmRevokeAndRelogin(activeCredentials, nonInteractive))
1391
- return;
1392
- try {
1393
- await revokeCredentials({
1394
- api: sdk2.context.api,
1395
- credentials: activeCredentials
1396
- });
1397
- } catch {
1398
- if (!await confirmLocalLoginReset(nonInteractive)) return;
1399
- await deleteStoredClientCredentials({
1400
- name: activeCredentials.name,
1401
- baseUrl: activeCredentials.baseUrl
1402
- });
1403
- }
1404
- } else if (hasLegacyJwtConfig()) {
1405
- if (!await confirmJwtMigration(nonInteractive)) return;
1406
- }
1407
- const { accessToken } = await runOauthFlow({
1408
- timeoutMs: timeoutSeconds * 1e3,
1409
- pkceCredentials,
1410
- baseUrl: credentialsBaseUrl
1411
- });
1412
- const scopedApi = getOrCreateApiClient({
1413
- credentials: accessToken,
1414
- baseUrl: credentialsBaseUrl
1415
- });
1416
- const profile = await getProfile(scopedApi);
1417
- console.log(`\u{1F464} Logged in as ${profile.email}`);
1418
- console.log(
1419
- "\nGenerating credentials so this machine can make authenticated requests on your behalf."
1420
- );
1421
- const credentialName = await promptCredentialsName(
1422
- profile.email,
1423
- nonInteractive
1424
- );
1425
- const useApprovals = options.useApprovals === true;
1426
- await setupClientCredentials({
1427
- api: scopedApi,
1428
- name: credentialName,
1429
- credentialsBaseUrl,
1430
- ...useApprovals && { policy: EMPTY_POLICY }
1431
- });
1432
- await bestEffortClearLegacyJwtState();
1433
- console.log(
1434
- `\u2705 Credentials "${credentialName}" created and set as default. You are ready to use the Zapier SDK.`
1435
- );
1436
- if (useApprovals) {
1437
- console.log("\u{1F510} Approvals are enabled for these credentials.");
1438
- }
1439
- 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" });
1440
1939
  }
1441
1940
  })
1442
1941
  );
@@ -3968,7 +4467,7 @@ var watchTriggerInboxCliPlugin = definePlugin(
3968
4467
  // package.json with { type: 'json' }
3969
4468
  var package_default = {
3970
4469
  name: "@zapier/zapier-sdk-cli",
3971
- version: "0.52.12"};
4470
+ version: "0.53.0"};
3972
4471
 
3973
4472
  // src/experimental.ts
3974
4473
  injectCliLogin(login_exports);
@@ -3984,7 +4483,7 @@ function createZapierCliSdk(options = {}) {
3984
4483
  ...sdkOptions,
3985
4484
  eventEmission: { ...sdkOptions.eventEmission, callContext: "cli" },
3986
4485
  callerPackage: { name: package_default.name, version: package_default.version }
3987
- }).use(extensionsContextPlugin).use(experimentalContextPlugin).use(generateAppTypesPlugin).use(buildManifestPlugin).use(bundleCodePlugin).use(getLoginConfigPathPlugin).use(addAppsPlugin).use(feedbackPlugin).use(curlPlugin).use(initPlugin).use(drainTriggerInboxCliPlugin, { override: true }).use(watchTriggerInboxCliPlugin, { override: true }).use(mcpPlugin).use(loginPlugin).use(logoutPlugin).use(cliOverridesPlugin, { override: true }).toSdk();
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();
3988
4487
  for (const ext of extensions) {
3989
4488
  try {
3990
4489
  addPlugin(sdk, ext);