@zapier/zapier-sdk-cli 0.1.1 → 0.2.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 (44) hide show
  1. package/dist/cli.js +13 -11
  2. package/dist/commands/configPath.d.ts +2 -0
  3. package/dist/commands/configPath.js +9 -0
  4. package/dist/commands/index.d.ts +4 -0
  5. package/dist/commands/index.js +4 -0
  6. package/dist/commands/login.d.ts +2 -0
  7. package/dist/commands/login.js +25 -0
  8. package/dist/commands/logout.d.ts +2 -0
  9. package/dist/commands/logout.js +16 -0
  10. package/dist/commands/whoami.d.ts +2 -0
  11. package/dist/commands/whoami.js +17 -0
  12. package/dist/utils/api/client.d.ts +15 -0
  13. package/dist/utils/api/client.js +27 -0
  14. package/dist/utils/auth/login.d.ts +2 -0
  15. package/dist/utils/auth/login.js +134 -0
  16. package/dist/utils/cli-generator.js +42 -48
  17. package/dist/utils/constants.d.ts +5 -0
  18. package/dist/utils/constants.js +6 -0
  19. package/dist/utils/getCallablePromise.d.ts +6 -0
  20. package/dist/utils/getCallablePromise.js +14 -0
  21. package/dist/utils/log.d.ts +7 -0
  22. package/dist/utils/log.js +16 -0
  23. package/dist/utils/pager.js +10 -20
  24. package/dist/utils/parameter-resolver.js +34 -41
  25. package/dist/utils/schema-formatter.js +11 -17
  26. package/dist/utils/serializeAsync.d.ts +2 -0
  27. package/dist/utils/serializeAsync.js +16 -0
  28. package/dist/utils/spinner.d.ts +1 -0
  29. package/dist/utils/spinner.js +13 -0
  30. package/package.json +8 -2
  31. package/src/cli.ts +13 -3
  32. package/src/commands/configPath.ts +10 -0
  33. package/src/commands/index.ts +4 -0
  34. package/src/commands/login.ts +34 -0
  35. package/src/commands/logout.ts +19 -0
  36. package/src/commands/whoami.ts +25 -0
  37. package/src/utils/api/client.ts +44 -0
  38. package/src/utils/auth/login.ts +190 -0
  39. package/src/utils/constants.ts +9 -0
  40. package/src/utils/getCallablePromise.ts +21 -0
  41. package/src/utils/log.ts +18 -0
  42. package/src/utils/serializeAsync.ts +26 -0
  43. package/src/utils/spinner.ts +16 -0
  44. package/tsconfig.json +2 -2
@@ -0,0 +1,190 @@
1
+ import open from "open";
2
+ import crypto from "node:crypto";
3
+ import express from "express";
4
+ import pkceChallenge from "pkce-challenge";
5
+
6
+ import {
7
+ AUTH_MODE_HEADER,
8
+ LOGIN_CLIENT_ID,
9
+ LOGIN_PORTS,
10
+ LOGIN_TIMEOUT_MS,
11
+ ZAPIER_BASE,
12
+ } from "../constants";
13
+ import { spinPromise } from "../spinner";
14
+ import log from "../log";
15
+ import api from "../api/client";
16
+ import getCallablePromise from "../getCallablePromise";
17
+ import { updateLogin, logout } from "@zapier/zapier-sdk";
18
+
19
+ const findAvailablePort = (): Promise<number> => {
20
+ return new Promise((resolve, reject) => {
21
+ let portIndex = 0;
22
+
23
+ const tryPort = (port: number) => {
24
+ const server = express().listen(port, () => {
25
+ server.close();
26
+ resolve(port);
27
+ });
28
+
29
+ server.on("error", (err: NodeJS.ErrnoException) => {
30
+ if (err.code === "EADDRINUSE") {
31
+ if (portIndex < LOGIN_PORTS.length) {
32
+ // Try next predefined port
33
+ tryPort(LOGIN_PORTS[portIndex++]);
34
+ } else {
35
+ // All configured ports are busy
36
+ reject(
37
+ new Error(
38
+ `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`,
39
+ ),
40
+ );
41
+ }
42
+ } else {
43
+ reject(err);
44
+ }
45
+ });
46
+ };
47
+
48
+ if (LOGIN_PORTS.length > 0) {
49
+ tryPort(LOGIN_PORTS[portIndex++]);
50
+ } else {
51
+ reject(new Error("No OAuth callback ports configured"));
52
+ }
53
+ });
54
+ };
55
+
56
+ const generateRandomString = () => {
57
+ const array = new Uint32Array(28);
58
+ crypto.getRandomValues(array);
59
+ return Array.from(array, (dec) =>
60
+ ("0" + dec.toString(16)).substring(-2),
61
+ ).join("");
62
+ };
63
+
64
+ const login = async (timeoutMs: number = LOGIN_TIMEOUT_MS): Promise<string> => {
65
+ // Force logout
66
+ logout();
67
+
68
+ // Find an available port
69
+ const availablePort = await findAvailablePort();
70
+ const redirectUri = `http://localhost:${availablePort}/oauth`;
71
+
72
+ log.info(`Using port ${availablePort} for OAuth callback`);
73
+
74
+ const { promise: promisedCode, resolve: setCode } =
75
+ getCallablePromise<string>();
76
+
77
+ const app = express();
78
+
79
+ app.get("/oauth", (req, res) => {
80
+ setCode(String(req.query.code));
81
+ // Set headers to prevent keep-alive
82
+ res.setHeader("Connection", "close");
83
+ res.end("You can now close this tab and return to the CLI.");
84
+ });
85
+
86
+ const server = app.listen(availablePort);
87
+
88
+ // Track connections to force close them if needed
89
+ const connections = new Set<any>();
90
+ server.on("connection", (conn) => {
91
+ connections.add(conn);
92
+ conn.on("close", () => connections.delete(conn));
93
+ });
94
+
95
+ // Set up signal handlers for graceful shutdown
96
+ const cleanup = () => {
97
+ server.close();
98
+ log.info("\n❌ Login cancelled by user");
99
+ process.exit(0);
100
+ };
101
+
102
+ process.on("SIGINT", cleanup);
103
+ process.on("SIGTERM", cleanup);
104
+
105
+ const { code_verifier: codeVerifier, code_challenge: codeChallenge } =
106
+ await pkceChallenge();
107
+
108
+ const authUrl = `${ZAPIER_BASE}/oauth/authorize/?${new URLSearchParams({
109
+ response_type: "code",
110
+ client_id: LOGIN_CLIENT_ID,
111
+ redirect_uri: redirectUri,
112
+ scope: "internal offline_access",
113
+ state: generateRandomString(),
114
+ code_challenge: codeChallenge,
115
+ code_challenge_method: "S256",
116
+ }).toString()}`;
117
+
118
+ log.info("Opening your browser to log in.");
119
+ log.info("If it doesn't open, visit:", authUrl);
120
+
121
+ open(authUrl);
122
+
123
+ try {
124
+ await spinPromise(
125
+ Promise.race([
126
+ promisedCode,
127
+ new Promise<string>((_resolve, reject) =>
128
+ setTimeout(() => {
129
+ reject(
130
+ new Error(
131
+ `Login timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
132
+ ),
133
+ );
134
+ }, timeoutMs),
135
+ ),
136
+ ]),
137
+ "Waiting for you to login and authorize",
138
+ );
139
+ } finally {
140
+ // Remove signal handlers
141
+ process.off("SIGINT", cleanup);
142
+ process.off("SIGTERM", cleanup);
143
+
144
+ // Close server with timeout and force-close connections if needed
145
+ await new Promise<void>((resolve) => {
146
+ const timeout = setTimeout(() => {
147
+ log.info("Server close timed out, forcing connection shutdown...");
148
+ // Force close all connections
149
+ connections.forEach((conn) => conn.destroy());
150
+ resolve();
151
+ }, 1000); // 1 second timeout
152
+
153
+ server.close(() => {
154
+ clearTimeout(timeout);
155
+ resolve();
156
+ });
157
+ });
158
+ }
159
+
160
+ log.info("Exchanging authorization code for tokens...");
161
+
162
+ const { data } = await api.post<{
163
+ access_token: string;
164
+ refresh_token: string;
165
+ expires_in: number;
166
+ }>(
167
+ `${ZAPIER_BASE}/oauth/token/`,
168
+ {
169
+ grant_type: "authorization_code",
170
+ code: await promisedCode,
171
+ redirect_uri: redirectUri,
172
+ client_id: LOGIN_CLIENT_ID,
173
+ code_verifier: codeVerifier,
174
+ },
175
+ {
176
+ headers: {
177
+ [AUTH_MODE_HEADER]: "no",
178
+ "Content-Type": "application/x-www-form-urlencoded",
179
+ },
180
+ },
181
+ );
182
+
183
+ updateLogin(data);
184
+
185
+ log.info("Token exchange completed successfully");
186
+
187
+ return data.access_token;
188
+ };
189
+
190
+ export default login;
@@ -0,0 +1,9 @@
1
+ export const ZAPIER_BASE = "https://zapier.com";
2
+
3
+ // OAuth client ID for zapier-sdk CLI
4
+ export const LOGIN_CLIENT_ID = "K5eEnRE9TTmSFATdkkWhKF8NOKwoiOnYAyIqJjae";
5
+
6
+ export const LOGIN_PORTS = [49505, 50575, 52804, 55981, 61010, 63851];
7
+ export const LOGIN_TIMEOUT_MS = 300000; // 5 minutes
8
+
9
+ export const AUTH_MODE_HEADER = "X-Auth";
@@ -0,0 +1,21 @@
1
+ const getCallablePromise = <T>(): {
2
+ promise: Promise<T>;
3
+ resolve: (value: T) => void;
4
+ reject: (reason: unknown) => void;
5
+ } => {
6
+ let resolve: (value: T) => void = () => {};
7
+ let reject: (reason: unknown) => void = () => {};
8
+
9
+ const promise = new Promise<T>((_resolve, _reject) => {
10
+ resolve = _resolve;
11
+ reject = _reject;
12
+ });
13
+
14
+ return {
15
+ promise,
16
+ resolve,
17
+ reject,
18
+ };
19
+ };
20
+
21
+ export default getCallablePromise;
@@ -0,0 +1,18 @@
1
+ import chalk from "chalk";
2
+
3
+ const log = {
4
+ info: (message: string, ...args: any[]) => {
5
+ console.log(chalk.blue("ℹ"), message, ...args);
6
+ },
7
+ error: (message: string, ...args: any[]) => {
8
+ console.error(chalk.red("✖"), message, ...args);
9
+ },
10
+ success: (message: string, ...args: any[]) => {
11
+ console.log(chalk.green("✓"), message, ...args);
12
+ },
13
+ warn: (message: string, ...args: any[]) => {
14
+ console.log(chalk.yellow("⚠"), message, ...args);
15
+ },
16
+ };
17
+
18
+ export default log;
@@ -0,0 +1,26 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const promises: Record<string, Promise<unknown>> = {};
4
+
5
+ const serializeAsync =
6
+ <R, A extends []>(
7
+ fn: (...args: A) => Promise<R>,
8
+ ): ((...args: A) => Promise<R>) =>
9
+ async (...args: A): Promise<R> => {
10
+ const hash = createHash("sha256")
11
+ .update(fn.toString(), "utf8")
12
+ .digest()
13
+ .toString("hex");
14
+
15
+ const promise = promises[hash];
16
+
17
+ if (!promise) {
18
+ promises[hash] = fn(...args).finally(() => {
19
+ delete promises[hash];
20
+ });
21
+ }
22
+
23
+ return promises[hash] as Promise<R>;
24
+ };
25
+
26
+ export default serializeAsync;
@@ -0,0 +1,16 @@
1
+ import ora from "ora";
2
+
3
+ export const spinPromise = async <T>(
4
+ promise: Promise<T>,
5
+ text: string,
6
+ ): Promise<T> => {
7
+ const spinner = ora(text).start();
8
+ try {
9
+ const result = await promise;
10
+ spinner.succeed();
11
+ return result;
12
+ } catch (error) {
13
+ spinner.fail();
14
+ throw error;
15
+ }
16
+ };
package/tsconfig.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
- "module": "CommonJS",
5
- "moduleResolution": "node",
4
+ "module": "ES2020",
5
+ "moduleResolution": "bundler",
6
6
  "strict": true,
7
7
  "noUnusedLocals": true,
8
8
  "noUnusedParameters": true,