@zapier/zapier-sdk-cli 0.1.1 → 0.2.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 (71) hide show
  1. package/dist/cli.js +15 -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 +16 -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/ensureValidToken.d.ts +7 -0
  15. package/dist/utils/auth/ensureValidToken.js +33 -0
  16. package/dist/utils/auth/getAuthState.d.ts +12 -0
  17. package/dist/utils/auth/getAuthState.js +17 -0
  18. package/dist/utils/auth/getJWT.d.ts +2 -0
  19. package/dist/utils/auth/getJWT.js +13 -0
  20. package/dist/utils/auth/getLoggedInUser.d.ts +6 -0
  21. package/dist/utils/auth/getLoggedInUser.js +47 -0
  22. package/dist/utils/auth/login.d.ts +2 -0
  23. package/dist/utils/auth/login.js +135 -0
  24. package/dist/utils/auth/logout.d.ts +2 -0
  25. package/dist/utils/auth/logout.js +7 -0
  26. package/dist/utils/auth/refreshJWT.d.ts +2 -0
  27. package/dist/utils/auth/refreshJWT.js +33 -0
  28. package/dist/utils/auth/updateLogin.d.ts +7 -0
  29. package/dist/utils/auth/updateLogin.js +7 -0
  30. package/dist/utils/cli-generator.js +42 -48
  31. package/dist/utils/config.d.ts +2 -0
  32. package/dist/utils/config.js +2 -0
  33. package/dist/utils/constants.d.ts +5 -0
  34. package/dist/utils/constants.js +6 -0
  35. package/dist/utils/getCallablePromise.d.ts +6 -0
  36. package/dist/utils/getCallablePromise.js +14 -0
  37. package/dist/utils/getConfigPath.d.ts +1 -0
  38. package/dist/utils/getConfigPath.js +4 -0
  39. package/dist/utils/log.d.ts +7 -0
  40. package/dist/utils/log.js +16 -0
  41. package/dist/utils/pager.js +10 -20
  42. package/dist/utils/parameter-resolver.js +34 -41
  43. package/dist/utils/schema-formatter.js +11 -17
  44. package/dist/utils/serializeAsync.d.ts +2 -0
  45. package/dist/utils/serializeAsync.js +16 -0
  46. package/dist/utils/spinner.d.ts +1 -0
  47. package/dist/utils/spinner.js +13 -0
  48. package/package.json +9 -2
  49. package/src/cli.ts +15 -3
  50. package/src/commands/configPath.ts +10 -0
  51. package/src/commands/index.ts +4 -0
  52. package/src/commands/login.ts +34 -0
  53. package/src/commands/logout.ts +19 -0
  54. package/src/commands/whoami.ts +20 -0
  55. package/src/utils/api/client.ts +44 -0
  56. package/src/utils/auth/ensureValidToken.ts +35 -0
  57. package/src/utils/auth/getAuthState.ts +36 -0
  58. package/src/utils/auth/getJWT.ts +20 -0
  59. package/src/utils/auth/getLoggedInUser.ts +68 -0
  60. package/src/utils/auth/login.ts +191 -0
  61. package/src/utils/auth/logout.ts +9 -0
  62. package/src/utils/auth/refreshJWT.ts +50 -0
  63. package/src/utils/auth/updateLogin.ts +19 -0
  64. package/src/utils/config.ts +3 -0
  65. package/src/utils/constants.ts +9 -0
  66. package/src/utils/getCallablePromise.ts +21 -0
  67. package/src/utils/getConfigPath.ts +5 -0
  68. package/src/utils/log.ts +18 -0
  69. package/src/utils/serializeAsync.ts +26 -0
  70. package/src/utils/spinner.ts +16 -0
  71. package/tsconfig.json +2 -2
@@ -1,10 +1,4 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.formatItemsFromSchema = formatItemsFromSchema;
7
- const chalk_1 = __importDefault(require("chalk"));
1
+ import chalk from "chalk";
8
2
  function getFormatMetadata(schema) {
9
3
  return schema?._def?.formatMeta;
10
4
  }
@@ -14,7 +8,7 @@ function getOutputSchema(schema) {
14
8
  // ============================================================================
15
9
  // Generic Schema-Driven Formatter
16
10
  // ============================================================================
17
- function formatItemsFromSchema(inputSchema, items) {
11
+ export function formatItemsFromSchema(inputSchema, items) {
18
12
  // Get the output schema and its format metadata
19
13
  const outputSchema = getOutputSchema(inputSchema);
20
14
  if (!outputSchema) {
@@ -37,9 +31,9 @@ function formatSingleItem(item, index, formatMeta) {
37
31
  // Get the formatted item from the format function
38
32
  const formatted = formatMeta.format(item);
39
33
  // Build the main title line
40
- let titleLine = `${chalk_1.default.gray(`${index + 1}.`)} ${chalk_1.default.cyan(formatted.title)}`;
34
+ let titleLine = `${chalk.gray(`${index + 1}.`)} ${chalk.cyan(formatted.title)}`;
41
35
  if (formatted.subtitle) {
42
- titleLine += ` ${chalk_1.default.gray(formatted.subtitle)}`;
36
+ titleLine += ` ${chalk.gray(formatted.subtitle)}`;
43
37
  }
44
38
  console.log(titleLine);
45
39
  // Format detail lines
@@ -52,25 +46,25 @@ function formatSingleItem(item, index, formatMeta) {
52
46
  function applyStyle(value, style) {
53
47
  switch (style) {
54
48
  case "dim":
55
- return chalk_1.default.dim(value);
49
+ return chalk.dim(value);
56
50
  case "accent":
57
- return chalk_1.default.magenta(value);
51
+ return chalk.magenta(value);
58
52
  case "warning":
59
- return chalk_1.default.red(value);
53
+ return chalk.red(value);
60
54
  case "success":
61
- return chalk_1.default.green(value);
55
+ return chalk.green(value);
62
56
  case "normal":
63
57
  default:
64
- return chalk_1.default.blue(value);
58
+ return chalk.blue(value);
65
59
  }
66
60
  }
67
61
  function formatItemsGeneric(items) {
68
62
  // Fallback formatting for items without schema metadata
69
63
  items.forEach((item, index) => {
70
64
  const name = item.name || item.key || item.id || "Item";
71
- console.log(`${chalk_1.default.gray(`${index + 1}.`)} ${chalk_1.default.cyan(name)}`);
65
+ console.log(`${chalk.gray(`${index + 1}.`)} ${chalk.cyan(name)}`);
72
66
  if (item.description) {
73
- console.log(` ${chalk_1.default.dim(item.description)}`);
67
+ console.log(` ${chalk.dim(item.description)}`);
74
68
  }
75
69
  console.log();
76
70
  });
@@ -0,0 +1,2 @@
1
+ declare const serializeAsync: <R, A extends []>(fn: (...args: A) => Promise<R>) => ((...args: A) => Promise<R>);
2
+ export default serializeAsync;
@@ -0,0 +1,16 @@
1
+ import { createHash } from "node:crypto";
2
+ const promises = {};
3
+ const serializeAsync = (fn) => async (...args) => {
4
+ const hash = createHash("sha256")
5
+ .update(fn.toString(), "utf8")
6
+ .digest()
7
+ .toString("hex");
8
+ const promise = promises[hash];
9
+ if (!promise) {
10
+ promises[hash] = fn(...args).finally(() => {
11
+ delete promises[hash];
12
+ });
13
+ }
14
+ return promises[hash];
15
+ };
16
+ export default serializeAsync;
@@ -0,0 +1 @@
1
+ export declare const spinPromise: <T>(promise: Promise<T>, text: string) => Promise<T>;
@@ -0,0 +1,13 @@
1
+ import ora from "ora";
2
+ export const spinPromise = async (promise, text) => {
3
+ const spinner = ora(text).start();
4
+ try {
5
+ const result = await promise;
6
+ spinner.succeed();
7
+ return result;
8
+ }
9
+ catch (error) {
10
+ spinner.fail();
11
+ throw error;
12
+ }
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zapier/zapier-sdk-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Command line interface for Zapier SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,13 +22,20 @@
22
22
  "chalk": "^5.3.0",
23
23
  "cli-table3": "^0.6.5",
24
24
  "commander": "^12.0.0",
25
+ "conf": "^14.0.0",
26
+ "express": "^5.1.0",
25
27
  "inquirer": "^12.6.3",
28
+ "jsonwebtoken": "^9.0.2",
29
+ "open": "^10.2.0",
26
30
  "ora": "^8.2.0",
31
+ "pkce-challenge": "^5.0.0",
27
32
  "zod": "^3.25.67",
28
- "@zapier/zapier-sdk": "0.1.1"
33
+ "@zapier/zapier-sdk": "0.2.0"
29
34
  },
30
35
  "devDependencies": {
36
+ "@types/express": "^5.0.3",
31
37
  "@types/inquirer": "^9.0.8",
38
+ "@types/jsonwebtoken": "^9.0.10",
32
39
  "@types/node": "^24.0.1",
33
40
  "typescript": "^5.8.3",
34
41
  "vitest": "^3.2.3"
package/src/cli.ts CHANGED
@@ -3,6 +3,13 @@
3
3
  import { Command } from "commander";
4
4
  import { createZapierSdk } from "@zapier/zapier-sdk";
5
5
  import { generateCliCommands } from "./utils/cli-generator";
6
+ import { ensureValidToken } from "./utils/auth/ensureValidToken";
7
+ import {
8
+ createLoginCommand,
9
+ createLogoutCommand,
10
+ createWhoamiCommand,
11
+ createConfigPathCommand,
12
+ } from "./commands";
6
13
 
7
14
  const program = new Command();
8
15
 
@@ -16,13 +23,18 @@ program
16
23
  const isDebugMode =
17
24
  process.env.DEBUG === "true" || process.argv.includes("--debug");
18
25
 
19
- // Create SDK instance for CLI operations
20
- // Auth will be resolved from environment variables or command options
26
+ // Create SDK instance with dynamic token resolution
21
27
  const sdk = createZapierSdk({
22
- // Token will be picked up from ZAPIER_TOKEN env var or provided via options
28
+ getToken: ensureValidToken,
23
29
  debug: isDebugMode,
24
30
  });
25
31
 
32
+ // Add auth commands before generating SDK commands
33
+ program.addCommand(createLoginCommand());
34
+ program.addCommand(createLogoutCommand());
35
+ program.addCommand(createWhoamiCommand());
36
+ program.addCommand(createConfigPathCommand());
37
+
26
38
  // Generate CLI commands from SDK schemas
27
39
  generateCliCommands(program, sdk);
28
40
 
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ import { getConfigPath } from "../utils/getConfigPath";
3
+
4
+ export function createConfigPathCommand(): Command {
5
+ return new Command("get-config-path")
6
+ .description("Show the path to the configuration file")
7
+ .action(async () => {
8
+ console.log(`Configuration file: ${getConfigPath()}`);
9
+ });
10
+ }
@@ -0,0 +1,4 @@
1
+ export { createLoginCommand } from "./login";
2
+ export { createLogoutCommand } from "./logout";
3
+ export { createWhoamiCommand } from "./whoami";
4
+ export { createConfigPathCommand } from "./configPath";
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ import login from "../utils/auth/login";
3
+ import getLoggedInUser from "../utils/auth/getLoggedInUser";
4
+
5
+ export function createLoginCommand(): Command {
6
+ return new Command("login")
7
+ .description("Log in to Zapier to access your account")
8
+ .option(
9
+ "--timeout <seconds>",
10
+ "Login timeout in seconds (default: 300)",
11
+ "300",
12
+ )
13
+ .action(async (options) => {
14
+ try {
15
+ const timeoutSeconds = parseInt(options.timeout, 10);
16
+ if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
17
+ throw new Error("Timeout must be a positive number");
18
+ }
19
+ await login(timeoutSeconds * 1000); // Convert to milliseconds
20
+ const user = await getLoggedInUser();
21
+
22
+ console.log(`✅ Successfully logged in as ${user.email}`);
23
+
24
+ // Force immediate exit to prevent hanging (especially in development with tsx)
25
+ setTimeout(() => process.exit(0), 100);
26
+ } catch (error) {
27
+ console.error(
28
+ "❌ Login failed:",
29
+ error instanceof Error ? error.message : "Unknown error",
30
+ );
31
+ process.exit(1);
32
+ }
33
+ });
34
+ }
@@ -0,0 +1,19 @@
1
+ import { Command } from "commander";
2
+ import logout from "../utils/auth/logout";
3
+
4
+ export function createLogoutCommand(): Command {
5
+ return new Command("logout")
6
+ .description("Log out of your Zapier account")
7
+ .action(async () => {
8
+ try {
9
+ logout();
10
+ console.log("✅ Successfully logged out");
11
+ } catch (error) {
12
+ console.error(
13
+ "❌ Logout failed:",
14
+ error instanceof Error ? error.message : "Unknown error",
15
+ );
16
+ process.exit(1);
17
+ }
18
+ });
19
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander";
2
+ import getLoggedInUser from "../utils/auth/getLoggedInUser";
3
+
4
+ export function createWhoamiCommand(): Command {
5
+ return new Command("whoami")
6
+ .description("Show current login status and user information")
7
+ .action(async () => {
8
+ try {
9
+ const user = await getLoggedInUser();
10
+ console.log(
11
+ `✅ Logged in as ${user.email} (Account ID: ${user.accountId})`,
12
+ );
13
+ } catch {
14
+ console.log(
15
+ "❌ Not logged in. Use 'zapier-sdk login' to authenticate.",
16
+ );
17
+ process.exit(1);
18
+ }
19
+ });
20
+ }
@@ -0,0 +1,44 @@
1
+ // Simple HTTP client for OAuth authentication
2
+ export interface ApiResponse<T = any> {
3
+ data: T;
4
+ status: number;
5
+ }
6
+
7
+ export const createApiClient = () => {
8
+ const post = async <T = any>(
9
+ url: string,
10
+ data: any,
11
+ options: {
12
+ headers?: Record<string, string>;
13
+ } = {},
14
+ ): Promise<ApiResponse<T>> => {
15
+ const { headers = {} } = options;
16
+
17
+ const response = await fetch(url, {
18
+ method: "POST",
19
+ headers: {
20
+ "Content-Type": "application/x-www-form-urlencoded",
21
+ Connection: "close",
22
+ ...headers,
23
+ },
24
+ body: new URLSearchParams(data),
25
+ });
26
+
27
+ if (!response.ok) {
28
+ throw new Error(`${response.status} ${response.statusText}`);
29
+ }
30
+
31
+ const responseData = await response.json();
32
+ return {
33
+ data: responseData,
34
+ status: response.status,
35
+ };
36
+ };
37
+
38
+ return {
39
+ post,
40
+ };
41
+ };
42
+
43
+ const api = createApiClient();
44
+ export default api;
@@ -0,0 +1,35 @@
1
+ import getAuthState from "./getAuthState";
2
+ import refreshJWT from "./refreshJWT";
3
+
4
+ /**
5
+ * Ensures we have a valid, non-expired JWT token.
6
+ * Will attempt to refresh expired tokens automatically.
7
+ * Falls back to ZAPIER_TOKEN environment variable if no stored login.
8
+ * Returns undefined if no token is available or refresh fails.
9
+ */
10
+ export const ensureValidToken = async (): Promise<string | undefined> => {
11
+ try {
12
+ const state = getAuthState();
13
+
14
+ if (state.status === "logged-out") {
15
+ // Fall back to environment variable if not logged in
16
+ return process.env.ZAPIER_TOKEN;
17
+ }
18
+
19
+ if (state.status === "expired") {
20
+ // Attempt to refresh the token
21
+ try {
22
+ return await refreshJWT();
23
+ } catch {
24
+ // If refresh fails, fall back to environment variable
25
+ return process.env.ZAPIER_TOKEN;
26
+ }
27
+ }
28
+
29
+ // Status is "logged-in", return the valid token
30
+ return state.jwt;
31
+ } catch {
32
+ // If any error occurs, fall back to environment variable
33
+ return process.env.ZAPIER_TOKEN;
34
+ }
35
+ };
@@ -0,0 +1,36 @@
1
+ import { config } from "../config";
2
+
3
+ type Status =
4
+ | {
5
+ status: "logged-in";
6
+ jwt: string;
7
+ refresh_token: string;
8
+ }
9
+ | {
10
+ status: "expired";
11
+ refresh_token: string;
12
+ }
13
+ | {
14
+ status: "logged-out";
15
+ };
16
+
17
+ const getAuthState = (): Status => {
18
+ const jwt = config.get("login_jwt") as string | undefined;
19
+ const refreshToken = config.get("login_refresh_token") as string | undefined;
20
+ const expiresAt = config.get("login_expires_at") as number | undefined;
21
+
22
+ if (!jwt || !refreshToken || !expiresAt) {
23
+ return { status: "logged-out" };
24
+ }
25
+
26
+ if (expiresAt > Date.now() + 30 * 1000) {
27
+ return { status: "logged-in", jwt, refresh_token: refreshToken };
28
+ }
29
+
30
+ return {
31
+ status: "expired",
32
+ refresh_token: refreshToken,
33
+ };
34
+ };
35
+
36
+ export default getAuthState;
@@ -0,0 +1,20 @@
1
+ import getAuthState from "./getAuthState";
2
+ import refreshJWT from "./refreshJWT";
3
+
4
+ const getJWT = async (): Promise<string> => {
5
+ const state = getAuthState();
6
+
7
+ if (state.status === "logged-out") {
8
+ throw new Error(
9
+ "Expected getJWT to only be called when user has logged in.",
10
+ );
11
+ }
12
+
13
+ if (state.status === "expired") {
14
+ return await refreshJWT();
15
+ }
16
+
17
+ return state.jwt;
18
+ };
19
+
20
+ export default getJWT;
@@ -0,0 +1,68 @@
1
+ import jsonwebtoken, { JwtPayload } from "jsonwebtoken";
2
+ import getJWT from "./getJWT";
3
+
4
+ const decodeJWTOrThrow = (jwt: unknown): JwtPayload => {
5
+ if (typeof jwt !== "string") {
6
+ throw new Error("Expected JWT to be a string");
7
+ }
8
+
9
+ const decodedJWT = jsonwebtoken.decode(jwt, { complete: true });
10
+
11
+ if (!decodedJWT) {
12
+ throw new Error("Could not decode JWT");
13
+ }
14
+
15
+ if (typeof decodedJWT.payload === "string") {
16
+ throw new Error("Did not expect JWT payload to be a string");
17
+ }
18
+
19
+ return decodedJWT;
20
+ };
21
+
22
+ const getLoggedInUser = async (): Promise<{
23
+ accountId: number;
24
+ customUserId: number;
25
+ email: string;
26
+ }> => {
27
+ const jwt = await getJWT();
28
+
29
+ let decodedJwt = decodeJWTOrThrow(jwt);
30
+
31
+ if (decodedJwt.payload["sub_type"] == "service") {
32
+ decodedJwt = decodeJWTOrThrow(decodedJwt.payload["njwt"]);
33
+ }
34
+
35
+ if (typeof decodedJwt.payload["zap:acc"] !== "string") {
36
+ throw new Error("JWT payload does not contain accountId");
37
+ }
38
+
39
+ const accountId = parseInt(decodedJwt.payload["zap:acc"], 10);
40
+ if (isNaN(accountId)) {
41
+ throw new Error("JWT accountId is not a number");
42
+ }
43
+
44
+ if (
45
+ decodedJwt.payload["sub_type"] !== "customuser" ||
46
+ typeof decodedJwt.payload["sub"] !== "string"
47
+ ) {
48
+ throw new Error("JWT payload does not contain customUserId");
49
+ }
50
+
51
+ const customUserId = parseInt(decodedJwt.payload["sub"], 10);
52
+ if (isNaN(customUserId)) {
53
+ throw new Error("JWT customUserId is not a number");
54
+ }
55
+
56
+ const email = decodedJwt.payload["zap:uname"];
57
+ if (typeof email !== "string") {
58
+ throw new Error("JWT payload does not contain email");
59
+ }
60
+
61
+ return {
62
+ accountId,
63
+ customUserId,
64
+ email,
65
+ };
66
+ };
67
+
68
+ export default getLoggedInUser;
@@ -0,0 +1,191 @@
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 from "./updateLogin";
18
+ import logout from "./logout";
19
+
20
+ const findAvailablePort = (): Promise<number> => {
21
+ return new Promise((resolve, reject) => {
22
+ let portIndex = 0;
23
+
24
+ const tryPort = (port: number) => {
25
+ const server = express().listen(port, () => {
26
+ server.close();
27
+ resolve(port);
28
+ });
29
+
30
+ server.on("error", (err: NodeJS.ErrnoException) => {
31
+ if (err.code === "EADDRINUSE") {
32
+ if (portIndex < LOGIN_PORTS.length) {
33
+ // Try next predefined port
34
+ tryPort(LOGIN_PORTS[portIndex++]);
35
+ } else {
36
+ // All configured ports are busy
37
+ reject(
38
+ new Error(
39
+ `All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`,
40
+ ),
41
+ );
42
+ }
43
+ } else {
44
+ reject(err);
45
+ }
46
+ });
47
+ };
48
+
49
+ if (LOGIN_PORTS.length > 0) {
50
+ tryPort(LOGIN_PORTS[portIndex++]);
51
+ } else {
52
+ reject(new Error("No OAuth callback ports configured"));
53
+ }
54
+ });
55
+ };
56
+
57
+ const generateRandomString = () => {
58
+ const array = new Uint32Array(28);
59
+ crypto.getRandomValues(array);
60
+ return Array.from(array, (dec) =>
61
+ ("0" + dec.toString(16)).substring(-2),
62
+ ).join("");
63
+ };
64
+
65
+ const login = async (timeoutMs: number = LOGIN_TIMEOUT_MS): Promise<string> => {
66
+ // Force logout
67
+ logout();
68
+
69
+ // Find an available port
70
+ const availablePort = await findAvailablePort();
71
+ const redirectUri = `http://localhost:${availablePort}/oauth`;
72
+
73
+ log.info(`Using port ${availablePort} for OAuth callback`);
74
+
75
+ const { promise: promisedCode, resolve: setCode } =
76
+ getCallablePromise<string>();
77
+
78
+ const app = express();
79
+
80
+ app.get("/oauth", (req, res) => {
81
+ setCode(String(req.query.code));
82
+ // Set headers to prevent keep-alive
83
+ res.setHeader("Connection", "close");
84
+ res.end("You can now close this tab and return to the CLI.");
85
+ });
86
+
87
+ const server = app.listen(availablePort);
88
+
89
+ // Track connections to force close them if needed
90
+ const connections = new Set<any>();
91
+ server.on("connection", (conn) => {
92
+ connections.add(conn);
93
+ conn.on("close", () => connections.delete(conn));
94
+ });
95
+
96
+ // Set up signal handlers for graceful shutdown
97
+ const cleanup = () => {
98
+ server.close();
99
+ log.info("\n❌ Login cancelled by user");
100
+ process.exit(0);
101
+ };
102
+
103
+ process.on("SIGINT", cleanup);
104
+ process.on("SIGTERM", cleanup);
105
+
106
+ const { code_verifier: codeVerifier, code_challenge: codeChallenge } =
107
+ await pkceChallenge();
108
+
109
+ const authUrl = `${ZAPIER_BASE}/oauth/authorize/?${new URLSearchParams({
110
+ response_type: "code",
111
+ client_id: LOGIN_CLIENT_ID,
112
+ redirect_uri: redirectUri,
113
+ scope: "internal offline_access",
114
+ state: generateRandomString(),
115
+ code_challenge: codeChallenge,
116
+ code_challenge_method: "S256",
117
+ }).toString()}`;
118
+
119
+ log.info("Opening your browser to log in.");
120
+ log.info("If it doesn't open, visit:", authUrl);
121
+
122
+ open(authUrl);
123
+
124
+ try {
125
+ await spinPromise(
126
+ Promise.race([
127
+ promisedCode,
128
+ new Promise<string>((_resolve, reject) =>
129
+ setTimeout(() => {
130
+ reject(
131
+ new Error(
132
+ `Login timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
133
+ ),
134
+ );
135
+ }, timeoutMs),
136
+ ),
137
+ ]),
138
+ "Waiting for you to login and authorize",
139
+ );
140
+ } finally {
141
+ // Remove signal handlers
142
+ process.off("SIGINT", cleanup);
143
+ process.off("SIGTERM", cleanup);
144
+
145
+ // Close server with timeout and force-close connections if needed
146
+ await new Promise<void>((resolve) => {
147
+ const timeout = setTimeout(() => {
148
+ log.info("Server close timed out, forcing connection shutdown...");
149
+ // Force close all connections
150
+ connections.forEach((conn) => conn.destroy());
151
+ resolve();
152
+ }, 1000); // 1 second timeout
153
+
154
+ server.close(() => {
155
+ clearTimeout(timeout);
156
+ resolve();
157
+ });
158
+ });
159
+ }
160
+
161
+ log.info("Exchanging authorization code for tokens...");
162
+
163
+ const { data } = await api.post<{
164
+ access_token: string;
165
+ refresh_token: string;
166
+ expires_in: number;
167
+ }>(
168
+ `${ZAPIER_BASE}/oauth/token/`,
169
+ {
170
+ grant_type: "authorization_code",
171
+ code: await promisedCode,
172
+ redirect_uri: redirectUri,
173
+ client_id: LOGIN_CLIENT_ID,
174
+ code_verifier: codeVerifier,
175
+ },
176
+ {
177
+ headers: {
178
+ [AUTH_MODE_HEADER]: "no",
179
+ "Content-Type": "application/x-www-form-urlencoded",
180
+ },
181
+ },
182
+ );
183
+
184
+ updateLogin(data);
185
+
186
+ log.info("Token exchange completed successfully");
187
+
188
+ return data.access_token;
189
+ };
190
+
191
+ export default login;
@@ -0,0 +1,9 @@
1
+ import { config } from "../config";
2
+
3
+ const logout = () => {
4
+ config.delete("login_jwt");
5
+ config.delete("login_refresh_token");
6
+ config.delete("login_expires_at");
7
+ };
8
+
9
+ export default logout;