@zapier/zapier-sdk-cli 0.52.10 → 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.
- package/CHANGELOG.md +56 -0
- package/README.md +39 -1
- package/dist/cli.cjs +972 -473
- package/dist/cli.mjs +973 -474
- package/dist/experimental.cjs +914 -424
- package/dist/experimental.d.mts +1 -1
- package/dist/experimental.d.ts +1 -1
- package/dist/experimental.mjs +910 -420
- package/dist/index.cjs +914 -424
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +910 -420
- package/dist/login.cjs +8 -10
- package/dist/login.d.mts +2 -10
- package/dist/login.d.ts +2 -10
- package/dist/login.mjs +5 -9
- package/dist/package.json +1 -1
- package/dist/{sdk-B3nKAZdN.d.mts → sdk-SOLizjno.d.mts} +54 -16
- package/dist/{sdk-B3nKAZdN.d.ts → sdk-SOLizjno.d.ts} +54 -16
- package/dist/src/experimental.js +30 -27
- package/dist/src/login/index.d.ts +1 -9
- package/dist/src/login/index.js +12 -14
- package/dist/src/plugins/add/index.d.ts +15 -15
- package/dist/src/plugins/add/index.js +1 -1
- package/dist/src/plugins/buildManifest/index.d.ts +2 -2
- package/dist/src/plugins/bundleCode/index.d.ts +1 -1
- package/dist/src/plugins/bundleCode/index.js +2 -1
- package/dist/src/plugins/cliOverrides/index.d.ts +5 -10
- package/dist/src/plugins/cliOverrides/index.js +2 -6
- package/dist/src/plugins/curl/index.d.ts +2 -2
- package/dist/src/plugins/curl/schemas.d.ts +2 -2
- package/dist/src/plugins/feedback/index.d.ts +1 -1
- package/dist/src/plugins/generateAppTypes/index.d.ts +11 -11
- package/dist/src/plugins/getLoginConfigPath/index.d.ts +1 -1
- package/dist/src/plugins/index.d.ts +2 -1
- package/dist/src/plugins/index.js +2 -1
- package/dist/src/plugins/init/index.d.ts +1 -1
- package/dist/src/plugins/login/index.d.ts +3 -16
- package/dist/src/plugins/login/index.js +3 -191
- package/dist/src/plugins/logout/index.d.ts +1 -1
- package/dist/src/plugins/mcp/index.d.ts +1 -1
- package/dist/src/plugins/signup/index.d.ts +25 -0
- package/dist/src/plugins/signup/index.js +12 -0
- package/dist/src/plugins/signup/schemas.d.ts +9 -0
- package/dist/src/plugins/signup/schemas.js +26 -0
- package/dist/src/plugins/signup/test-harness.d.ts +34 -0
- package/dist/src/plugins/signup/test-harness.js +74 -0
- package/dist/src/sdk.js +32 -20
- package/dist/src/types/sdk.d.ts +2 -1
- package/dist/src/utils/auth/account-auth.d.ts +32 -0
- package/dist/src/utils/auth/account-auth.js +265 -0
- package/dist/src/utils/auth/oauth-callback.d.ts +6 -0
- package/dist/src/utils/auth/oauth-callback.js +28 -0
- package/dist/src/utils/auth/oauth-errors.d.ts +2 -0
- package/dist/src/utils/auth/oauth-errors.js +39 -0
- package/dist/src/utils/auth/oauth-flow.d.ts +31 -6
- package/dist/src/utils/auth/oauth-flow.js +258 -106
- package/dist/src/utils/auth/oauth-transaction.d.ts +35 -0
- package/dist/src/utils/auth/oauth-transaction.js +69 -0
- package/dist/src/utils/cli-generator.js +14 -7
- package/dist/src/utils/cli-renderer.d.ts +13 -3
- package/dist/src/utils/cli-renderer.js +27 -20
- package/dist/src/utils/log.js +9 -4
- package/dist/src/utils/non-interactive.d.ts +5 -4
- package/dist/src/utils/non-interactive.js +6 -5
- package/dist/src/utils/parameter-resolver.js +3 -1
- package/dist/src/utils/schema-formatter.d.ts +2 -2
- package/dist/src/utils/schema-formatter.js +4 -30
- package/dist/src/utils/version-checker.js +8 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { type PkceCredentials } from "../../login";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { buildBrowserAuthUrl, OAUTH_LOOPBACK_HOST, type OauthTokens } from "./oauth-transaction";
|
|
3
|
+
export { buildBrowserAuthUrl, OAUTH_LOOPBACK_HOST };
|
|
4
|
+
export type { OauthFlowEntryPoint, OauthTokens } from "./oauth-transaction";
|
|
5
|
+
export type OauthFlowProgressEvent = {
|
|
6
|
+
type: "browser_opening";
|
|
7
|
+
url: string;
|
|
8
|
+
} | {
|
|
9
|
+
type: "browser_opened";
|
|
10
|
+
url: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "browser_open_failed";
|
|
13
|
+
url: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: "callback_waiting";
|
|
17
|
+
} | {
|
|
18
|
+
type: "callback_accepted";
|
|
19
|
+
} | {
|
|
20
|
+
type: "token_exchange_started";
|
|
21
|
+
} | {
|
|
22
|
+
type: "token_exchange_completed";
|
|
23
|
+
};
|
|
7
24
|
export interface RunOauthFlowOptions {
|
|
8
25
|
timeoutMs?: number;
|
|
9
26
|
pkceCredentials?: PkceCredentials;
|
|
10
27
|
baseUrl?: string;
|
|
28
|
+
silent?: boolean;
|
|
29
|
+
onProgress?: (event: OauthFlowProgressEvent) => void;
|
|
30
|
+
recoveryMessage?: string;
|
|
31
|
+
}
|
|
32
|
+
interface RunSignupOauthFlowOptions extends RunOauthFlowOptions {
|
|
33
|
+
headless?: boolean;
|
|
34
|
+
interactive?: boolean;
|
|
11
35
|
}
|
|
12
|
-
export declare function
|
|
36
|
+
export declare function runLoginOauthFlow(options: RunOauthFlowOptions): Promise<OauthTokens>;
|
|
37
|
+
export declare function runSignupOauthFlow(options: RunSignupOauthFlowOptions): Promise<OauthTokens>;
|
|
@@ -1,90 +1,223 @@
|
|
|
1
|
-
import open from "open";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
1
|
import express from "express";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import api from "../api/client";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import open from "open";
|
|
4
|
+
import { LOGIN_PORTS, LOGIN_TIMEOUT_MS } from "../constants";
|
|
5
|
+
import { ZapierCliUserCancellationError, ZapierCliValidationError, } from "../errors";
|
|
9
6
|
import getCallablePromise from "../getCallablePromise";
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
7
|
+
import log from "../log";
|
|
8
|
+
import { spinPromise } from "../spinner";
|
|
9
|
+
import { getCallbackCode } from "./oauth-callback";
|
|
10
|
+
import { buildBrowserAuthUrl, exchangeOauthCode, OAUTH_LOOPBACK_HOST, prepareOauthTransaction, } from "./oauth-transaction";
|
|
11
|
+
export { buildBrowserAuthUrl, OAUTH_LOOPBACK_HOST };
|
|
12
|
+
class OauthFlowTimeoutError extends Error {
|
|
13
|
+
constructor(timeoutMs) {
|
|
14
|
+
super("OAuth flow timed out");
|
|
15
|
+
this.timeoutMs = timeoutMs;
|
|
16
|
+
this.name = "OauthFlowTimeoutError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
class OauthAuthorizationDeniedError extends Error {
|
|
20
|
+
constructor(reason) {
|
|
21
|
+
super("OAuth authorization denied");
|
|
22
|
+
this.reason = reason;
|
|
23
|
+
this.name = "OauthAuthorizationDeniedError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function findAvailablePort() {
|
|
13
27
|
return new Promise((resolve, reject) => {
|
|
14
28
|
let portIndex = 0;
|
|
15
29
|
const tryPort = (port) => {
|
|
16
|
-
const server = express().listen(port, () => {
|
|
30
|
+
const server = express().listen(port, OAUTH_LOOPBACK_HOST, () => {
|
|
17
31
|
server.close();
|
|
18
32
|
resolve(port);
|
|
19
33
|
});
|
|
20
34
|
server.on("error", (err) => {
|
|
21
|
-
if (err.code === "EADDRINUSE") {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
reject(new Error(`All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`));
|
|
27
|
-
}
|
|
35
|
+
if (err.code === "EADDRINUSE" && portIndex < LOGIN_PORTS.length) {
|
|
36
|
+
tryPort(LOGIN_PORTS[portIndex++]);
|
|
37
|
+
}
|
|
38
|
+
else if (err.code === "EADDRINUSE") {
|
|
39
|
+
reject(new Error(`All configured OAuth callback ports are busy: ${LOGIN_PORTS.join(", ")}. Please try again later or close applications using these ports.`));
|
|
28
40
|
}
|
|
29
41
|
else {
|
|
30
42
|
reject(err);
|
|
31
43
|
}
|
|
32
44
|
});
|
|
33
45
|
};
|
|
34
|
-
if (LOGIN_PORTS.length > 0)
|
|
46
|
+
if (LOGIN_PORTS.length > 0)
|
|
35
47
|
tryPort(LOGIN_PORTS[portIndex++]);
|
|
36
|
-
|
|
37
|
-
else {
|
|
48
|
+
else
|
|
38
49
|
reject(new Error("No OAuth callback ports configured"));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function runLoginOauthFlow(options) {
|
|
53
|
+
return runOauthFlowEntryPoint({
|
|
54
|
+
...options,
|
|
55
|
+
entryPoint: "login",
|
|
56
|
+
authAction: "log in",
|
|
57
|
+
flowName: "Login",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function runSignupOauthFlow(options) {
|
|
61
|
+
if (options.headless) {
|
|
62
|
+
return runOauthFlowEntryPoint({
|
|
63
|
+
...options,
|
|
64
|
+
entryPoint: "signup",
|
|
65
|
+
authAction: "sign up",
|
|
66
|
+
flowName: "Signup",
|
|
67
|
+
headless: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return runOauthFlowEntryPoint({
|
|
71
|
+
...options,
|
|
72
|
+
entryPoint: "signup",
|
|
73
|
+
authAction: "sign up",
|
|
74
|
+
flowName: "Signup",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function runOauthFlowEntryPoint({ flowName, ...options }) {
|
|
78
|
+
try {
|
|
79
|
+
return options.headless
|
|
80
|
+
? await runHeadlessSignupOauthFlow(options)
|
|
81
|
+
: await runOauthFlow(options);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof OauthFlowTimeoutError) {
|
|
85
|
+
throw new Error(withRecoveryMessage(`${flowName} timed out after ${Math.round(error.timeoutMs / 1000)} seconds.`, options.recoveryMessage));
|
|
39
86
|
}
|
|
87
|
+
if (error instanceof OauthAuthorizationDeniedError) {
|
|
88
|
+
throw new Error(withRecoveryMessage(`Authorization denied: ${error.reason}.`, options.recoveryMessage));
|
|
89
|
+
}
|
|
90
|
+
if (error instanceof ZapierCliUserCancellationError && !options.silent) {
|
|
91
|
+
log.info(`\n❌ ${flowName} cancelled by user`);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function withRecoveryMessage(message, recoveryMessage) {
|
|
97
|
+
return recoveryMessage ? `${message} ${recoveryMessage}` : message;
|
|
98
|
+
}
|
|
99
|
+
async function runOauthFlow({ timeoutMs = LOGIN_TIMEOUT_MS, pkceCredentials, baseUrl, entryPoint, authAction, silent = false, onProgress, }) {
|
|
100
|
+
const port = await findAvailablePort();
|
|
101
|
+
if (!silent)
|
|
102
|
+
log.info(`Using port ${port} for OAuth callback`);
|
|
103
|
+
const transaction = await prepareOauthTransaction({
|
|
104
|
+
pkceCredentials,
|
|
105
|
+
baseUrl,
|
|
106
|
+
redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
|
|
107
|
+
entryPoint,
|
|
108
|
+
});
|
|
109
|
+
const code = await collectLocalCallbackCode({
|
|
110
|
+
transaction,
|
|
111
|
+
timeoutMs,
|
|
112
|
+
authAction,
|
|
113
|
+
silent,
|
|
114
|
+
onProgress,
|
|
40
115
|
});
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
116
|
+
onProgress?.({ type: "callback_accepted" });
|
|
117
|
+
if (!silent)
|
|
118
|
+
log.info("Exchanging authorization code for tokens...");
|
|
119
|
+
onProgress?.({ type: "token_exchange_started" });
|
|
120
|
+
const tokens = await exchangeOauthCode({ ...transaction, code });
|
|
121
|
+
if (!silent)
|
|
122
|
+
log.info("Token exchange completed successfully");
|
|
123
|
+
onProgress?.({ type: "token_exchange_completed" });
|
|
124
|
+
return tokens;
|
|
125
|
+
}
|
|
126
|
+
async function readHeadlessCallbackUrl({ timeoutMs, interactive, recoveryMessage, }) {
|
|
127
|
+
const timeoutMessage = withRecoveryMessage(`Signup timed out after ${Math.round(timeoutMs / 1000)} seconds.`, recoveryMessage);
|
|
128
|
+
const missingCallbackUrlMessage = withRecoveryMessage("Paste the final OAuth callback URL from your browser.", recoveryMessage);
|
|
129
|
+
// Keep TTY echo enabled for paste feedback; the PKCE code is single-use, verifier-protected, and exchanged immediately.
|
|
130
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
131
|
+
const abortController = new AbortController();
|
|
132
|
+
const timeoutTimer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
133
|
+
const readUrl = interactive
|
|
134
|
+
? rl.question("Paste the final OAuth callback URL: ", {
|
|
135
|
+
signal: abortController.signal,
|
|
136
|
+
})
|
|
137
|
+
: new Promise((resolve, reject) => {
|
|
138
|
+
let settled = false;
|
|
139
|
+
const settleResolve = (value) => {
|
|
140
|
+
settled = true;
|
|
141
|
+
resolve(value);
|
|
142
|
+
};
|
|
143
|
+
const settleReject = (error) => {
|
|
144
|
+
if (settled)
|
|
145
|
+
return;
|
|
146
|
+
settled = true;
|
|
147
|
+
reject(error);
|
|
148
|
+
};
|
|
149
|
+
abortController.signal.addEventListener("abort", () => settleReject(new Error(timeoutMessage)), { once: true });
|
|
150
|
+
rl.once("line", settleResolve);
|
|
151
|
+
rl.once("close", () => settleReject(new ZapierCliValidationError(missingCallbackUrlMessage)));
|
|
152
|
+
rl.once("error", settleReject);
|
|
153
|
+
});
|
|
154
|
+
try {
|
|
155
|
+
return await readUrl.catch((error) => {
|
|
156
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
157
|
+
throw new Error(timeoutMessage);
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
clearTimeout(timeoutTimer);
|
|
164
|
+
rl.close();
|
|
50
165
|
}
|
|
51
|
-
return `${scope} offline_access`;
|
|
52
166
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
167
|
+
async function runHeadlessSignupOauthFlow({ timeoutMs = LOGIN_TIMEOUT_MS, pkceCredentials, baseUrl, interactive = true, onProgress, recoveryMessage, }) {
|
|
168
|
+
const port = LOGIN_PORTS[0];
|
|
169
|
+
if (port === undefined) {
|
|
170
|
+
throw new Error("No OAuth callback ports configured");
|
|
171
|
+
}
|
|
172
|
+
const transaction = await prepareOauthTransaction({
|
|
173
|
+
pkceCredentials,
|
|
57
174
|
baseUrl,
|
|
175
|
+
redirectUri: `http://${OAUTH_LOOPBACK_HOST}:${port}/oauth`,
|
|
176
|
+
entryPoint: "signup",
|
|
177
|
+
});
|
|
178
|
+
console.log("Use this mode when signing up from a machine that has no browser.");
|
|
179
|
+
console.log("Open this signup URL in a browser on another machine:");
|
|
180
|
+
console.log(transaction.browserAuthUrl);
|
|
181
|
+
console.log(`When the browser lands on ${transaction.redirectUri} and cannot connect, paste the full final URL back here.`);
|
|
182
|
+
const callbackUrl = await readHeadlessCallbackUrl({
|
|
183
|
+
timeoutMs,
|
|
184
|
+
interactive,
|
|
185
|
+
recoveryMessage,
|
|
58
186
|
});
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
187
|
+
const code = getCallbackCode({
|
|
188
|
+
callbackUrl,
|
|
189
|
+
transaction,
|
|
190
|
+
recoveryMessage,
|
|
191
|
+
});
|
|
192
|
+
onProgress?.({ type: "callback_accepted" });
|
|
193
|
+
console.log("Exchanging authorization code for tokens...");
|
|
194
|
+
onProgress?.({ type: "token_exchange_started" });
|
|
195
|
+
const tokens = await exchangeOauthCode({ ...transaction, code });
|
|
196
|
+
onProgress?.({ type: "token_exchange_completed" });
|
|
197
|
+
return tokens;
|
|
198
|
+
}
|
|
199
|
+
async function collectLocalCallbackCode({ transaction, timeoutMs, authAction, silent, onProgress, }) {
|
|
200
|
+
const { promise, resolve, reject } = getCallablePromise();
|
|
201
|
+
const app = express();
|
|
202
|
+
app.get("/oauth", (req, res) => {
|
|
67
203
|
res.setHeader("Connection", "close");
|
|
68
|
-
if (req.query.state !==
|
|
69
|
-
rejectCode(new Error("OAuth state mismatch — possible CSRF"));
|
|
204
|
+
if (req.query.state !== transaction.state) {
|
|
70
205
|
res.status(400).end("Invalid state. You can close this tab.");
|
|
71
|
-
return;
|
|
72
206
|
}
|
|
73
|
-
if (req.query.error) {
|
|
74
|
-
|
|
75
|
-
rejectCode(new Error(`Authorization denied: ${desc}`));
|
|
207
|
+
else if (req.query.error) {
|
|
208
|
+
reject(new OauthAuthorizationDeniedError(String(req.query.error_description ?? req.query.error)));
|
|
76
209
|
res.end("Authorization was denied. You can close this tab.");
|
|
77
|
-
return;
|
|
78
210
|
}
|
|
79
|
-
if (!req.query.code) {
|
|
80
|
-
|
|
211
|
+
else if (!req.query.code) {
|
|
212
|
+
reject(new Error("No authorization code received"));
|
|
81
213
|
res.end("No authorization code received. You can close this tab.");
|
|
82
|
-
return;
|
|
83
214
|
}
|
|
84
|
-
|
|
85
|
-
|
|
215
|
+
else {
|
|
216
|
+
resolve(String(req.query.code));
|
|
217
|
+
res.end("You can now close this tab and return to the CLI.");
|
|
218
|
+
}
|
|
86
219
|
});
|
|
87
|
-
const server =
|
|
220
|
+
const server = app.listen(Number(new URL(transaction.redirectUri).port), OAUTH_LOOPBACK_HOST);
|
|
88
221
|
const connections = new Set();
|
|
89
222
|
server.on("connection", (conn) => {
|
|
90
223
|
connections.add(conn);
|
|
@@ -92,70 +225,89 @@ export async function runOauthFlow({ timeoutMs = LOGIN_TIMEOUT_MS, pkceCredentia
|
|
|
92
225
|
});
|
|
93
226
|
const cleanup = () => {
|
|
94
227
|
server.close();
|
|
95
|
-
|
|
96
|
-
rejectCode(new ZapierCliUserCancellationError());
|
|
228
|
+
reject(new ZapierCliUserCancellationError());
|
|
97
229
|
};
|
|
98
230
|
process.on("SIGINT", cleanup);
|
|
99
231
|
process.on("SIGTERM", cleanup);
|
|
100
|
-
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
101
|
-
const authUrl = `${authorizeUrl}?${new URLSearchParams({
|
|
102
|
-
response_type: "code",
|
|
103
|
-
client_id: clientId,
|
|
104
|
-
redirect_uri: redirectUri,
|
|
105
|
-
scope,
|
|
106
|
-
state: oauthState,
|
|
107
|
-
code_challenge: codeChallenge,
|
|
108
|
-
code_challenge_method: "S256",
|
|
109
|
-
}).toString()}`;
|
|
110
|
-
log.info("Opening your browser to log in.");
|
|
111
|
-
log.info("If it doesn't open, visit:", authUrl);
|
|
112
|
-
open(authUrl);
|
|
113
232
|
let timeoutTimer;
|
|
114
233
|
try {
|
|
115
|
-
await
|
|
116
|
-
|
|
117
|
-
|
|
234
|
+
await waitForServerListening(server);
|
|
235
|
+
await openBrowser({ transaction, authAction, silent, onProgress });
|
|
236
|
+
const waitForCode = Promise.race([
|
|
237
|
+
promise,
|
|
238
|
+
new Promise((_resolve, rejectTimeout) => {
|
|
118
239
|
timeoutTimer = setTimeout(() => {
|
|
119
|
-
|
|
240
|
+
rejectTimeout(new OauthFlowTimeoutError(timeoutMs));
|
|
120
241
|
}, timeoutMs);
|
|
121
242
|
}),
|
|
122
|
-
])
|
|
243
|
+
]);
|
|
244
|
+
onProgress?.({ type: "callback_waiting" });
|
|
245
|
+
return silent
|
|
246
|
+
? await waitForCode
|
|
247
|
+
: await spinPromise(waitForCode, `Waiting for you to ${authAction} and authorize`);
|
|
123
248
|
}
|
|
124
249
|
finally {
|
|
125
|
-
if (timeoutTimer)
|
|
250
|
+
if (timeoutTimer)
|
|
126
251
|
clearTimeout(timeoutTimer);
|
|
127
|
-
}
|
|
128
252
|
process.off("SIGINT", cleanup);
|
|
129
253
|
process.off("SIGTERM", cleanup);
|
|
130
|
-
await
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
254
|
+
await closeServer({ server, connections, silent });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function waitForServerListening(server) {
|
|
258
|
+
if (server.listening)
|
|
259
|
+
return;
|
|
260
|
+
await new Promise((resolve, reject) => {
|
|
261
|
+
const cleanup = () => {
|
|
262
|
+
server.off("listening", handleListening);
|
|
263
|
+
server.off("error", handleError);
|
|
264
|
+
};
|
|
265
|
+
const handleListening = () => {
|
|
266
|
+
cleanup();
|
|
267
|
+
resolve();
|
|
268
|
+
};
|
|
269
|
+
const handleError = (error) => {
|
|
270
|
+
cleanup();
|
|
271
|
+
reject(error);
|
|
272
|
+
};
|
|
273
|
+
server.once("listening", handleListening);
|
|
274
|
+
server.once("error", handleError);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function openBrowser({ transaction, authAction, silent, onProgress, }) {
|
|
278
|
+
if (!silent) {
|
|
279
|
+
log.info(`Opening your browser to ${authAction}.`);
|
|
280
|
+
log.info("If it doesn't open, visit:", transaction.browserAuthUrl);
|
|
281
|
+
}
|
|
282
|
+
onProgress?.({ type: "browser_opening", url: transaction.browserAuthUrl });
|
|
283
|
+
try {
|
|
284
|
+
await open(transaction.browserAuthUrl);
|
|
285
|
+
onProgress?.({ type: "browser_opened", url: transaction.browserAuthUrl });
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
289
|
+
if (!silent) {
|
|
290
|
+
log.info(`Browser did not open automatically to ${authAction}: ${reason}`);
|
|
291
|
+
log.info("Visit this URL manually:", transaction.browserAuthUrl);
|
|
292
|
+
}
|
|
293
|
+
onProgress?.({
|
|
294
|
+
type: "browser_open_failed",
|
|
295
|
+
url: transaction.browserAuthUrl,
|
|
296
|
+
reason,
|
|
140
297
|
});
|
|
141
298
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
299
|
+
}
|
|
300
|
+
async function closeServer({ server, connections, silent, }) {
|
|
301
|
+
await new Promise((resolve) => {
|
|
302
|
+
const timeout = setTimeout(() => {
|
|
303
|
+
if (!silent)
|
|
304
|
+
log.info("Server close timed out, forcing connection shutdown...");
|
|
305
|
+
connections.forEach((conn) => conn.destroy());
|
|
306
|
+
resolve();
|
|
307
|
+
}, 1000);
|
|
308
|
+
server.close(() => {
|
|
309
|
+
clearTimeout(timeout);
|
|
310
|
+
resolve();
|
|
311
|
+
});
|
|
154
312
|
});
|
|
155
|
-
log.info("Token exchange completed successfully");
|
|
156
|
-
return {
|
|
157
|
-
accessToken: data.access_token,
|
|
158
|
-
refreshToken: data.refresh_token,
|
|
159
|
-
expiresIn: data.expires_in,
|
|
160
|
-
};
|
|
161
313
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type PkceCredentials } from "../../login";
|
|
2
|
+
export declare const OAUTH_LOOPBACK_HOST = "localhost";
|
|
3
|
+
export interface OauthTokens {
|
|
4
|
+
accessToken: string;
|
|
5
|
+
refreshToken: string;
|
|
6
|
+
expiresIn: number;
|
|
7
|
+
}
|
|
8
|
+
export type OauthFlowEntryPoint = "login" | "signup";
|
|
9
|
+
interface PrepareOauthTransactionOptions {
|
|
10
|
+
pkceCredentials?: PkceCredentials;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
entryPoint?: OauthFlowEntryPoint;
|
|
14
|
+
}
|
|
15
|
+
export interface OauthTransaction {
|
|
16
|
+
browserAuthUrl: string;
|
|
17
|
+
clientId: string;
|
|
18
|
+
codeVerifier: string;
|
|
19
|
+
redirectUri: string;
|
|
20
|
+
state: string;
|
|
21
|
+
tokenUrl: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function buildBrowserAuthUrl({ authorizeUrl, entryPoint, }: {
|
|
24
|
+
authorizeUrl: string;
|
|
25
|
+
entryPoint?: OauthFlowEntryPoint;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function prepareOauthTransaction({ pkceCredentials, baseUrl, redirectUri, entryPoint, }: PrepareOauthTransactionOptions): Promise<OauthTransaction>;
|
|
28
|
+
export declare function exchangeOauthCode({ tokenUrl, code, redirectUri, clientId, codeVerifier, }: {
|
|
29
|
+
tokenUrl: string;
|
|
30
|
+
code: string;
|
|
31
|
+
redirectUri: string;
|
|
32
|
+
clientId: string;
|
|
33
|
+
codeVerifier: string;
|
|
34
|
+
}): Promise<OauthTokens>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import pkceChallenge from "pkce-challenge";
|
|
3
|
+
import { AUTH_MODE_HEADER } from "../constants";
|
|
4
|
+
import api from "../api/client";
|
|
5
|
+
import { getPkceLoginConfig } from "../../login";
|
|
6
|
+
export const OAUTH_LOOPBACK_HOST = "localhost";
|
|
7
|
+
export function buildBrowserAuthUrl({ authorizeUrl, entryPoint = "login", }) {
|
|
8
|
+
if (entryPoint === "login")
|
|
9
|
+
return authorizeUrl;
|
|
10
|
+
const parsedAuthorizeUrl = new URL(authorizeUrl);
|
|
11
|
+
const signupUrl = new URL("/sign-up", parsedAuthorizeUrl);
|
|
12
|
+
signupUrl.searchParams.set("skipOnboarding", "true");
|
|
13
|
+
signupUrl.searchParams.set("next", `${parsedAuthorizeUrl.pathname}${parsedAuthorizeUrl.search}`);
|
|
14
|
+
return signupUrl.toString();
|
|
15
|
+
}
|
|
16
|
+
function generateRandomString() {
|
|
17
|
+
const array = new Uint32Array(28);
|
|
18
|
+
crypto.getRandomValues(array);
|
|
19
|
+
return Array.from(array, (dec) => ("0" + dec.toString(16)).slice(-2)).join("");
|
|
20
|
+
}
|
|
21
|
+
function ensureOfflineAccess(scope) {
|
|
22
|
+
if (scope.includes("offline_access"))
|
|
23
|
+
return scope;
|
|
24
|
+
return `${scope} offline_access`;
|
|
25
|
+
}
|
|
26
|
+
export async function prepareOauthTransaction({ pkceCredentials, baseUrl, redirectUri, entryPoint = "login", }) {
|
|
27
|
+
const { clientId, tokenUrl, authorizeUrl } = getPkceLoginConfig({
|
|
28
|
+
credentials: pkceCredentials,
|
|
29
|
+
baseUrl,
|
|
30
|
+
});
|
|
31
|
+
const { code_verifier: codeVerifier, code_challenge: codeChallenge } = await pkceChallenge();
|
|
32
|
+
const state = generateRandomString();
|
|
33
|
+
const authUrl = `${authorizeUrl}?${new URLSearchParams({
|
|
34
|
+
response_type: "code",
|
|
35
|
+
client_id: clientId,
|
|
36
|
+
redirect_uri: redirectUri,
|
|
37
|
+
scope: ensureOfflineAccess(pkceCredentials?.scope || "internal credentials"),
|
|
38
|
+
state,
|
|
39
|
+
code_challenge: codeChallenge,
|
|
40
|
+
code_challenge_method: "S256",
|
|
41
|
+
}).toString()}`;
|
|
42
|
+
return {
|
|
43
|
+
browserAuthUrl: buildBrowserAuthUrl({ authorizeUrl: authUrl, entryPoint }),
|
|
44
|
+
clientId,
|
|
45
|
+
codeVerifier,
|
|
46
|
+
redirectUri,
|
|
47
|
+
state,
|
|
48
|
+
tokenUrl,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function exchangeOauthCode({ tokenUrl, code, redirectUri, clientId, codeVerifier, }) {
|
|
52
|
+
const { data } = await api.post(tokenUrl, {
|
|
53
|
+
grant_type: "authorization_code",
|
|
54
|
+
code,
|
|
55
|
+
redirect_uri: redirectUri,
|
|
56
|
+
client_id: clientId,
|
|
57
|
+
code_verifier: codeVerifier,
|
|
58
|
+
}, {
|
|
59
|
+
headers: {
|
|
60
|
+
[AUTH_MODE_HEADER]: "no",
|
|
61
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
accessToken: data.access_token,
|
|
66
|
+
refreshToken: data.refresh_token,
|
|
67
|
+
expiresIn: data.expires_in,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -472,14 +472,20 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
|
|
|
472
472
|
const startTime = Date.now();
|
|
473
473
|
let success = true;
|
|
474
474
|
let errorMessage = null;
|
|
475
|
-
|
|
475
|
+
// `resolvedParams` is populated below either by `resolveParameters` or
|
|
476
|
+
// by copying `rawParams`. The renderer captures this object by
|
|
477
|
+
// reference and reads from it inside formatter callbacks, so we
|
|
478
|
+
// mutate it in place (via `Object.assign`) once params are resolved
|
|
479
|
+
// rather than reassigning the binding, which would leave the
|
|
480
|
+
// renderer holding the empty initial object.
|
|
481
|
+
const resolvedParams = {};
|
|
476
482
|
// The last argument is always the command object with parsed options
|
|
477
483
|
const commandObj = args[args.length - 1];
|
|
478
484
|
const options = commandObj.opts();
|
|
479
485
|
// interactiveMode is true by default; --json disables it
|
|
480
486
|
const interactiveMode = !options.json;
|
|
481
487
|
const renderer = interactiveMode
|
|
482
|
-
? createInteractiveRenderer()
|
|
488
|
+
? createInteractiveRenderer({ sdk, params: resolvedParams })
|
|
483
489
|
: createJsonRenderer();
|
|
484
490
|
try {
|
|
485
491
|
emitDeprecationWarning({
|
|
@@ -509,15 +515,16 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
|
|
|
509
515
|
}
|
|
510
516
|
if (schema && !usesInputParameters) {
|
|
511
517
|
const resolver = new SchemaParameterResolver();
|
|
512
|
-
|
|
518
|
+
const resolved = (await resolver.resolveParameters(schema, rawParams, sdk, functionInfo.name, {
|
|
513
519
|
interactiveMode,
|
|
514
520
|
debug: !!options.debug ||
|
|
515
521
|
process.env.DEBUG === "true" ||
|
|
516
522
|
process.argv.includes("--debug"),
|
|
517
523
|
}));
|
|
524
|
+
Object.assign(resolvedParams, resolved);
|
|
518
525
|
}
|
|
519
526
|
else {
|
|
520
|
-
resolvedParams
|
|
527
|
+
Object.assign(resolvedParams, rawParams);
|
|
521
528
|
}
|
|
522
529
|
const confirm = functionInfo.confirm;
|
|
523
530
|
let confirmMessageAfter;
|
|
@@ -543,7 +550,7 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
|
|
|
543
550
|
sdkResult,
|
|
544
551
|
maxItems,
|
|
545
552
|
});
|
|
546
|
-
renderer.renderCollectedList(allItems, {
|
|
553
|
+
await renderer.renderCollectedList(allItems, {
|
|
547
554
|
maxItems,
|
|
548
555
|
userSpecifiedMaxItems: hasUserSpecifiedMaxItems,
|
|
549
556
|
functionInfo,
|
|
@@ -575,7 +582,7 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
|
|
|
575
582
|
await renderer.renderResponse(normalizedResult.value);
|
|
576
583
|
}
|
|
577
584
|
else if (normalizedResult.kind === "list") {
|
|
578
|
-
renderer.renderCollectedList(normalizedResult.data, {
|
|
585
|
+
await renderer.renderCollectedList(normalizedResult.data, {
|
|
579
586
|
maxItems: resolvedParams.maxItems,
|
|
580
587
|
userSpecifiedMaxItems: hasUserSpecifiedMaxItems,
|
|
581
588
|
functionInfo,
|
|
@@ -635,7 +642,7 @@ function createCommandConfig(cliCommandName, functionInfo, sdk) {
|
|
|
635
642
|
description,
|
|
636
643
|
parameters,
|
|
637
644
|
handler,
|
|
638
|
-
hidden: functionInfo.
|
|
645
|
+
hidden: !!functionInfo.deprecation,
|
|
639
646
|
aliases: functionInfo.aliases,
|
|
640
647
|
supportsJsonOutput: functionInfo.supportsJsonOutput,
|
|
641
648
|
};
|