@zapier/zapier-sdk-cli 0.47.0 → 0.48.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/cli.cjs +559 -84
  4. package/dist/cli.mjs +559 -84
  5. package/dist/experimental.cjs +561 -86
  6. package/dist/experimental.mjs +560 -85
  7. package/dist/index.cjs +562 -87
  8. package/dist/index.mjs +561 -86
  9. package/dist/login.cjs +94 -25
  10. package/dist/login.d.mts +29 -2
  11. package/dist/login.d.ts +29 -2
  12. package/dist/login.mjs +90 -25
  13. package/dist/package.json +1 -1
  14. package/dist/src/login/config.d.ts +4 -0
  15. package/dist/src/login/config.js +21 -0
  16. package/dist/src/login/credentials-revoke.d.ts +13 -0
  17. package/dist/src/login/credentials-revoke.js +48 -0
  18. package/dist/src/login/credentials-store.d.ts +33 -0
  19. package/dist/src/login/credentials-store.js +142 -0
  20. package/dist/src/login/index.d.ts +5 -2
  21. package/dist/src/login/index.js +11 -27
  22. package/dist/src/login/legacy-jwt.d.ts +4 -0
  23. package/dist/src/login/legacy-jwt.js +18 -0
  24. package/dist/src/plugins/auth/credentials-base-url.d.ts +11 -0
  25. package/dist/src/plugins/auth/credentials-base-url.js +24 -0
  26. package/dist/src/plugins/login/index.d.ts +6 -1
  27. package/dist/src/plugins/login/index.js +154 -14
  28. package/dist/src/plugins/logout/index.d.ts +14 -0
  29. package/dist/src/plugins/logout/index.js +35 -3
  30. package/dist/src/utils/auth/client-credentials.d.ts +16 -0
  31. package/dist/src/utils/auth/client-credentials.js +53 -0
  32. package/dist/src/utils/auth/oauth-flow.d.ts +12 -0
  33. package/dist/src/utils/auth/{login.js → oauth-flow.js} +36 -58
  34. package/dist/src/utils/retry.d.ts +5 -0
  35. package/dist/src/utils/retry.js +21 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +3 -3
  38. package/dist/src/utils/auth/login.d.ts +0 -7
@@ -0,0 +1,12 @@
1
+ import { type PkceCredentials } from "../../login";
2
+ export interface OauthTokens {
3
+ accessToken: string;
4
+ refreshToken: string;
5
+ expiresIn: number;
6
+ }
7
+ export interface RunOauthFlowOptions {
8
+ timeoutMs?: number;
9
+ pkceCredentials?: PkceCredentials;
10
+ baseUrl?: string;
11
+ }
12
+ export declare function runOauthFlow({ timeoutMs, pkceCredentials, baseUrl, }: RunOauthFlowOptions): Promise<OauthTokens>;
@@ -7,8 +7,7 @@ import { spinPromise } from "../spinner";
7
7
  import log from "../log";
8
8
  import api from "../api/client";
9
9
  import getCallablePromise from "../getCallablePromise";
10
- import inquirer from "inquirer";
11
- import { updateLogin, logout, getPkceLoginConfig, getLoginStorageMode, getConfigPath, } from "../../login";
10
+ import { getPkceLoginConfig } from "../../login";
12
11
  import { ZapierCliUserCancellationError } from "../errors";
13
12
  const findAvailablePort = () => {
14
13
  return new Promise((resolve, reject) => {
@@ -21,11 +20,9 @@ const findAvailablePort = () => {
21
20
  server.on("error", (err) => {
22
21
  if (err.code === "EADDRINUSE") {
23
22
  if (portIndex < LOGIN_PORTS.length) {
24
- // Try next predefined port
25
23
  tryPort(LOGIN_PORTS[portIndex++]);
26
24
  }
27
25
  else {
28
- // All configured ports are busy
29
26
  reject(new Error(`All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`));
30
27
  }
31
28
  }
@@ -45,41 +42,54 @@ const findAvailablePort = () => {
45
42
  const generateRandomString = () => {
46
43
  const array = new Uint32Array(28);
47
44
  crypto.getRandomValues(array);
48
- return Array.from(array, (dec) => ("0" + dec.toString(16)).substring(-2)).join("");
45
+ return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join("");
49
46
  };
50
- // Ensure offline_access is included in scope for PKCE (needed for refresh tokens)
51
47
  function ensureOfflineAccess(scope) {
52
48
  if (scope.includes("offline_access")) {
53
49
  return scope;
54
50
  }
55
51
  return `${scope} offline_access`;
56
52
  }
57
- const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
53
+ // Does not persist anything caller decides what to do with the tokens.
54
+ export async function runOauthFlow({ timeoutMs = LOGIN_TIMEOUT_MS, pkceCredentials, baseUrl, }) {
58
55
  const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
59
- credentials,
56
+ credentials: pkceCredentials,
57
+ baseUrl,
60
58
  });
61
- const scope = ensureOfflineAccess(credentials?.scope || "internal credentials");
62
- await logout();
63
- // Find an available port
59
+ const scope = ensureOfflineAccess(pkceCredentials?.scope || "internal credentials");
64
60
  const availablePort = await findAvailablePort();
65
61
  const redirectUri = `http://localhost:${availablePort}/oauth`;
66
62
  log.info(`Using port ${availablePort} for OAuth callback`);
67
63
  const { promise: promisedCode, resolve: setCode, reject: rejectCode, } = getCallablePromise();
68
- const app = express();
69
- app.get("/oauth", (req, res) => {
70
- setCode(String(req.query.code));
71
- // Set headers to prevent keep-alive
64
+ const oauthState = generateRandomString();
65
+ const expressApp = express();
66
+ expressApp.get("/oauth", (req, res) => {
72
67
  res.setHeader("Connection", "close");
68
+ if (req.query.state !== oauthState) {
69
+ rejectCode(new Error("OAuth state mismatch — possible CSRF"));
70
+ res.status(400).end("Invalid state. You can close this tab.");
71
+ return;
72
+ }
73
+ if (req.query.error) {
74
+ const desc = req.query.error_description ?? req.query.error;
75
+ rejectCode(new Error(`Authorization denied: ${desc}`));
76
+ res.end("Authorization was denied. You can close this tab.");
77
+ return;
78
+ }
79
+ if (!req.query.code) {
80
+ rejectCode(new Error("No authorization code received"));
81
+ res.end("No authorization code received. You can close this tab.");
82
+ return;
83
+ }
84
+ setCode(String(req.query.code));
73
85
  res.end("You can now close this tab and return to the CLI.");
74
86
  });
75
- const server = app.listen(availablePort);
76
- // Track connections to force close them if needed
87
+ const server = expressApp.listen(availablePort);
77
88
  const connections = new Set();
78
89
  server.on("connection", (conn) => {
79
90
  connections.add(conn);
80
91
  conn.on("close", () => connections.delete(conn));
81
92
  });
82
- // Set up signal handlers for graceful shutdown
83
93
  const cleanup = () => {
84
94
  server.close();
85
95
  log.info("\n❌ Login cancelled by user");
@@ -93,14 +103,13 @@ const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
93
103
  client_id: clientId,
94
104
  redirect_uri: redirectUri,
95
105
  scope,
96
- state: generateRandomString(),
106
+ state: oauthState,
97
107
  code_challenge: codeChallenge,
98
108
  code_challenge_method: "S256",
99
109
  }).toString()}`;
100
110
  log.info("Opening your browser to log in.");
101
111
  log.info("If it doesn't open, visit:", authUrl);
102
112
  open(authUrl);
103
- // Track the timeout timer so we can cancel it after login succeeds
104
113
  let timeoutTimer;
105
114
  try {
106
115
  await spinPromise(Promise.race([
@@ -113,21 +122,17 @@ const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
113
122
  ]), "Waiting for you to login and authorize");
114
123
  }
115
124
  finally {
116
- // Clear the timeout timer to prevent it from keeping the process alive
117
125
  if (timeoutTimer) {
118
126
  clearTimeout(timeoutTimer);
119
127
  }
120
- // Remove signal handlers
121
128
  process.off("SIGINT", cleanup);
122
129
  process.off("SIGTERM", cleanup);
123
- // Close server with timeout and force-close connections if needed
124
130
  await new Promise((resolve) => {
125
131
  const timeout = setTimeout(() => {
126
132
  log.info("Server close timed out, forcing connection shutdown...");
127
- // Force close all connections
128
133
  connections.forEach((conn) => conn.destroy());
129
134
  resolve();
130
- }, 1000); // 1 second timeout
135
+ }, 1000);
131
136
  server.close(() => {
132
137
  clearTimeout(timeout);
133
138
  resolve();
@@ -147,37 +152,10 @@ const login = async ({ timeoutMs = LOGIN_TIMEOUT_MS, credentials, }) => {
147
152
  "Content-Type": "application/x-www-form-urlencoded",
148
153
  },
149
154
  });
150
- let targetStorage;
151
- if (getLoginStorageMode() === "config") {
152
- const { upgrade } = await inquirer.prompt([
153
- {
154
- type: "confirm",
155
- name: "upgrade",
156
- message: "Would you like to upgrade to system keychain storage? " +
157
- "This is recommended to securely store your credentials. " +
158
- "However, note that older SDK/CLI versions will NOT be able to read these credentials, " +
159
- "so you will want to upgrade them to the latest version.",
160
- default: true,
161
- },
162
- ]);
163
- targetStorage = upgrade ? "keychain" : "config";
164
- }
165
- else {
166
- targetStorage = "keychain";
167
- }
168
- try {
169
- await updateLogin(data, { storage: targetStorage });
170
- }
171
- catch (err) {
172
- if (targetStorage === "keychain") {
173
- log.warn(`Could not store credentials in system keychain. Storing in plaintext at ${getConfigPath()}.`);
174
- await updateLogin(data, { storage: "config" });
175
- }
176
- else {
177
- throw err;
178
- }
179
- }
180
155
  log.info("Token exchange completed successfully");
181
- return data.access_token;
182
- };
183
- export default login;
156
+ return {
157
+ accessToken: data.access_token,
158
+ refreshToken: data.refresh_token,
159
+ expiresIn: data.expires_in,
160
+ };
161
+ }
@@ -0,0 +1,5 @@
1
+ export declare function withRetry<T>({ action, attempts, initialDelayMs, }: {
2
+ action: () => Promise<T>;
3
+ attempts?: number;
4
+ initialDelayMs?: number;
5
+ }): Promise<T>;
@@ -0,0 +1,21 @@
1
+ function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ export async function withRetry({ action, attempts = 3, initialDelayMs = 100, }) {
5
+ if (attempts <= 0) {
6
+ throw new Error("withRetry: attempts must be greater than 0");
7
+ }
8
+ let lastError;
9
+ for (let i = 0; i < attempts; i++) {
10
+ try {
11
+ return await action();
12
+ }
13
+ catch (err) {
14
+ lastError = err;
15
+ if (i < attempts - 1) {
16
+ await sleep(initialDelayMs * 2 ** i);
17
+ }
18
+ }
19
+ }
20
+ throw lastError;
21
+ }