@zapier/zapier-sdk-cli 0.46.1 → 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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/cli.cjs +569 -84
  4. package/dist/cli.mjs +569 -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/parameter-resolver.js +13 -0
  35. package/dist/src/utils/retry.d.ts +5 -0
  36. package/dist/src/utils/retry.js +21 -0
  37. package/dist/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +3 -3
  39. 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
+ }
@@ -801,12 +801,22 @@ export class SchemaParameterResolver {
801
801
  const choices = [...initialChoices];
802
802
  let nextCursor = initialCursor;
803
803
  const LOAD_MORE_SENTINEL = Symbol("LOAD_MORE");
804
+ const CUSTOM_VALUE_SENTINEL = Symbol("CUSTOM_VALUE");
804
805
  // Progressive loading loop
805
806
  while (true) {
806
807
  const promptChoices = choices.map((choice) => ({
807
808
  name: choice.label,
808
809
  value: choice.value,
809
810
  }));
811
+ // Pin "(Enter custom value)" to the top for single-select fields so
812
+ // users can supply values not in the fetched dropdown (e.g. dynamic
813
+ // dropdowns that don't return every valid value).
814
+ if (!fieldMeta.isMultiSelect) {
815
+ promptChoices.unshift({
816
+ name: chalk.dim("(Enter custom value)"),
817
+ value: CUSTOM_VALUE_SENTINEL,
818
+ });
819
+ }
810
820
  // Add "(Load more...)" option if there are more pages
811
821
  if (nextCursor) {
812
822
  promptChoices.push({
@@ -834,6 +844,9 @@ export class SchemaParameterResolver {
834
844
  };
835
845
  const answer = await inquirer.prompt([promptConfig]);
836
846
  let selectedValue = answer[fieldMeta.key];
847
+ if (selectedValue === CUSTOM_VALUE_SENTINEL) {
848
+ return await this.promptFreeForm(fieldMeta);
849
+ }
837
850
  // Check if user selected "Load more..."
838
851
  const wantsMore = fieldMeta.isMultiSelect
839
852
  ? Array.isArray(selectedValue) &&
@@ -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
+ }