@zapier/zapier-sdk-cli 0.52.12 → 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 +13 -0
- package/README.md +20 -0
- package/dist/cli.cjs +896 -395
- package/dist/cli.mjs +897 -396
- package/dist/experimental.cjs +897 -398
- package/dist/experimental.d.mts +1 -1
- package/dist/experimental.d.ts +1 -1
- package/dist/experimental.mjs +896 -397
- package/dist/index.cjs +898 -399
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +897 -398
- package/dist/package.json +1 -1
- package/dist/{sdk-Sa1HjzUj.d.mts → sdk-SOLizjno.d.mts} +40 -2
- package/dist/{sdk-Sa1HjzUj.d.ts → sdk-SOLizjno.d.ts} +40 -2
- package/dist/src/experimental.js +2 -1
- package/dist/src/plugins/index.d.ts +1 -0
- package/dist/src/plugins/index.js +1 -0
- package/dist/src/plugins/login/index.d.ts +2 -15
- package/dist/src/plugins/login/index.js +3 -191
- 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 +2 -1
- 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/non-interactive.d.ts +5 -4
- package/dist/src/utils/non-interactive.js +6 -5
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* Resolve whether a CLI invocation should run non-interactively.
|
|
3
3
|
*
|
|
4
4
|
* A command is non-interactive when the user explicitly opts out via
|
|
5
|
-
* `--non-interactive
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `--non-interactive`, OR when stdin or stdout isn't a TTY (CI, piped output,
|
|
6
|
+
* etc.). The TTY check matters because prompting against a non-TTY stream
|
|
7
|
+
* hangs or crashes — every plugin that gates inquirer prompts on this flag
|
|
8
|
+
* needs the same answer.
|
|
9
9
|
*/
|
|
10
10
|
export declare function resolveNonInteractive(options: {
|
|
11
11
|
nonInteractive?: boolean;
|
|
12
|
+
/** @deprecated Use `nonInteractive` instead. */
|
|
12
13
|
skipPrompts?: boolean;
|
|
13
14
|
}): boolean;
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* Resolve whether a CLI invocation should run non-interactively.
|
|
3
3
|
*
|
|
4
4
|
* A command is non-interactive when the user explicitly opts out via
|
|
5
|
-
* `--non-interactive
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* `--non-interactive`, OR when stdin or stdout isn't a TTY (CI, piped output,
|
|
6
|
+
* etc.). The TTY check matters because prompting against a non-TTY stream
|
|
7
|
+
* hangs or crashes — every plugin that gates inquirer prompts on this flag
|
|
8
|
+
* needs the same answer.
|
|
9
9
|
*/
|
|
10
10
|
export function resolveNonInteractive(options) {
|
|
11
|
-
return (
|
|
11
|
+
return (options.nonInteractive === true ||
|
|
12
|
+
options.skipPrompts === true ||
|
|
12
13
|
!process.stdin.isTTY ||
|
|
13
14
|
!process.stdout.isTTY);
|
|
14
15
|
}
|