ctx7 0.4.5 → 0.5.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/dist/index.js +198 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1155,17 +1155,88 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
|
|
|
1155
1155
|
url.searchParams.set("response_type", "code");
|
|
1156
1156
|
return url.toString();
|
|
1157
1157
|
}
|
|
1158
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1159
|
+
var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
1160
|
+
function shouldUseDeviceFlow() {
|
|
1161
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
|
|
1162
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
1163
|
+
return true;
|
|
1164
|
+
}
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
async function startDeviceAuthorization(baseUrl3, clientId) {
|
|
1168
|
+
const params = new URLSearchParams({ client_id: clientId });
|
|
1169
|
+
try {
|
|
1170
|
+
const hostname2 = os.hostname();
|
|
1171
|
+
if (hostname2) params.set("hostname", hostname2);
|
|
1172
|
+
} catch {
|
|
1173
|
+
}
|
|
1174
|
+
const response = await fetch(`${baseUrl3}/api/oauth/device/code`, {
|
|
1175
|
+
method: "POST",
|
|
1176
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1177
|
+
body: params.toString()
|
|
1178
|
+
});
|
|
1179
|
+
if (!response.ok) {
|
|
1180
|
+
const err = await response.json().catch(() => ({}));
|
|
1181
|
+
throw new Error(err.error_description || err.error || "Failed to start device authorization");
|
|
1182
|
+
}
|
|
1183
|
+
return await response.json();
|
|
1184
|
+
}
|
|
1185
|
+
async function pollDeviceToken(baseUrl3, clientId, deviceCode) {
|
|
1186
|
+
let response;
|
|
1187
|
+
try {
|
|
1188
|
+
response = await fetch(`${baseUrl3}/api/oauth/device/token`, {
|
|
1189
|
+
method: "POST",
|
|
1190
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1191
|
+
body: new URLSearchParams({
|
|
1192
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
1193
|
+
device_code: deviceCode,
|
|
1194
|
+
client_id: clientId
|
|
1195
|
+
}).toString()
|
|
1196
|
+
});
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
return {
|
|
1199
|
+
status: "transient",
|
|
1200
|
+
errorMessage: error instanceof Error ? error.message : "network error"
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
if (response.ok) {
|
|
1204
|
+
const tokens = await response.json();
|
|
1205
|
+
return { status: "approved", tokens };
|
|
1206
|
+
}
|
|
1207
|
+
if (response.status >= 500) {
|
|
1208
|
+
const err2 = await response.json().catch(() => ({}));
|
|
1209
|
+
return {
|
|
1210
|
+
status: "transient",
|
|
1211
|
+
errorMessage: err2.error_description || err2.error || `HTTP ${response.status}`
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
const err = await response.json().catch(() => ({}));
|
|
1215
|
+
switch (err.error) {
|
|
1216
|
+
case "authorization_pending":
|
|
1217
|
+
return { status: "pending" };
|
|
1218
|
+
case "slow_down":
|
|
1219
|
+
return { status: "slow_down" };
|
|
1220
|
+
case "access_denied":
|
|
1221
|
+
return { status: "denied" };
|
|
1222
|
+
case "expired_token":
|
|
1223
|
+
return { status: "expired" };
|
|
1224
|
+
default:
|
|
1225
|
+
throw new Error(err.error_description || err.error || "Device token poll failed");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1158
1228
|
|
|
1159
1229
|
// src/commands/auth.ts
|
|
1160
1230
|
import pc4 from "picocolors";
|
|
1161
1231
|
import ora from "ora";
|
|
1162
1232
|
import open from "open";
|
|
1233
|
+
import boxen from "boxen";
|
|
1163
1234
|
var baseUrl2 = "https://context7.com";
|
|
1164
1235
|
function setAuthBaseUrl(url) {
|
|
1165
1236
|
baseUrl2 = url;
|
|
1166
1237
|
}
|
|
1167
1238
|
function registerAuthCommands(program2) {
|
|
1168
|
-
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
1239
|
+
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").option("--device", "Force device-code flow (use on SSH / headless hosts)").action(async (options) => {
|
|
1169
1240
|
await loginCommand(options);
|
|
1170
1241
|
});
|
|
1171
1242
|
program2.command("logout").description("Log out of Context7").action(() => {
|
|
@@ -1175,7 +1246,131 @@ function registerAuthCommands(program2) {
|
|
|
1175
1246
|
await whoamiCommand();
|
|
1176
1247
|
});
|
|
1177
1248
|
}
|
|
1178
|
-
|
|
1249
|
+
function renderDeviceCodeBox(userCode, verificationUri, verificationUriComplete) {
|
|
1250
|
+
const codeLine = `${pc4.dim("Your one-time code:")}
|
|
1251
|
+
|
|
1252
|
+
${pc4.green(pc4.bold(userCode))}`;
|
|
1253
|
+
const linkLine = verificationUriComplete ? `${pc4.dim("Open this link to approve:")}
|
|
1254
|
+
${pc4.cyan(verificationUriComplete)}
|
|
1255
|
+
|
|
1256
|
+
${pc4.dim("Or visit")} ${pc4.cyan(verificationUri)} ${pc4.dim("and enter the code above.")}` : `${pc4.dim("Visit:")} ${pc4.cyan(verificationUri)}`;
|
|
1257
|
+
return boxen(`${codeLine}
|
|
1258
|
+
|
|
1259
|
+
${linkLine}`, {
|
|
1260
|
+
title: "Sign in to Context7",
|
|
1261
|
+
titleAlignment: "left",
|
|
1262
|
+
padding: 1,
|
|
1263
|
+
margin: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
1264
|
+
borderStyle: "round",
|
|
1265
|
+
borderColor: "gray"
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
function waitForEnter(prompt) {
|
|
1269
|
+
if (!process.stdin.isTTY) return Promise.resolve();
|
|
1270
|
+
return new Promise((resolve3) => {
|
|
1271
|
+
process.stdout.write(` ${pc4.dim(prompt)} `);
|
|
1272
|
+
const onData = (chunk) => {
|
|
1273
|
+
if (chunk[0] === 3) {
|
|
1274
|
+
process.stdin.removeListener("data", onData);
|
|
1275
|
+
process.stdin.setRawMode?.(false);
|
|
1276
|
+
process.stdin.pause();
|
|
1277
|
+
process.stdout.write("\n");
|
|
1278
|
+
process.exit(130);
|
|
1279
|
+
}
|
|
1280
|
+
process.stdin.removeListener("data", onData);
|
|
1281
|
+
process.stdin.setRawMode?.(false);
|
|
1282
|
+
process.stdin.pause();
|
|
1283
|
+
process.stdout.write("\n");
|
|
1284
|
+
resolve3();
|
|
1285
|
+
};
|
|
1286
|
+
process.stdin.setRawMode?.(true);
|
|
1287
|
+
process.stdin.resume();
|
|
1288
|
+
process.stdin.on("data", onData);
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async function announceIdentity(accessToken) {
|
|
1292
|
+
try {
|
|
1293
|
+
const whoami = await fetchWhoami(accessToken);
|
|
1294
|
+
const name = whoami.email || whoami.name;
|
|
1295
|
+
if (!name) return "Login successful!";
|
|
1296
|
+
const team = whoami.teamspace?.name;
|
|
1297
|
+
return team ? `Logged in as ${pc4.bold(name)} ${pc4.dim(`(${team})`)}` : `Logged in as ${pc4.bold(name)}`;
|
|
1298
|
+
} catch {
|
|
1299
|
+
return "Login successful!";
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
async function performDeviceLogin(openBrowser = true) {
|
|
1303
|
+
const spinner = ora("Preparing login...").start();
|
|
1304
|
+
let authorization;
|
|
1305
|
+
try {
|
|
1306
|
+
authorization = await startDeviceAuthorization(baseUrl2, CLI_CLIENT_ID);
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
spinner.fail(pc4.red("Login failed"));
|
|
1309
|
+
if (error instanceof Error) console.error(pc4.red(error.message));
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
spinner.stop();
|
|
1313
|
+
console.log(
|
|
1314
|
+
renderDeviceCodeBox(
|
|
1315
|
+
authorization.user_code,
|
|
1316
|
+
authorization.verification_uri,
|
|
1317
|
+
authorization.verification_uri_complete
|
|
1318
|
+
)
|
|
1319
|
+
);
|
|
1320
|
+
const target = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
1321
|
+
if (openBrowser) {
|
|
1322
|
+
await waitForEnter("Press Enter to open the browser, or Ctrl-C to quit...");
|
|
1323
|
+
try {
|
|
1324
|
+
await open(target);
|
|
1325
|
+
} catch {
|
|
1326
|
+
console.log(pc4.dim(` Couldn't open a browser \u2014 visit the link above manually.`));
|
|
1327
|
+
}
|
|
1328
|
+
} else {
|
|
1329
|
+
console.log(pc4.dim(" Open the link above in any browser to continue."));
|
|
1330
|
+
console.log("");
|
|
1331
|
+
}
|
|
1332
|
+
const waitingSpinner = ora({ text: "Waiting for authorization...", indent: 2 }).start();
|
|
1333
|
+
const deadline = Date.now() + authorization.expires_in * 1e3;
|
|
1334
|
+
let intervalMs = (authorization.interval ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1e3;
|
|
1335
|
+
while (Date.now() < deadline) {
|
|
1336
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
1337
|
+
try {
|
|
1338
|
+
const result = await pollDeviceToken(baseUrl2, CLI_CLIENT_ID, authorization.device_code);
|
|
1339
|
+
if (result.status === "approved" && result.tokens) {
|
|
1340
|
+
saveTokens(result.tokens);
|
|
1341
|
+
const successText = await announceIdentity(result.tokens.access_token);
|
|
1342
|
+
waitingSpinner.succeed(pc4.green(successText));
|
|
1343
|
+
return result.tokens.access_token;
|
|
1344
|
+
}
|
|
1345
|
+
if (result.status === "slow_down") {
|
|
1346
|
+
intervalMs += 5e3;
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
if (result.status === "denied") {
|
|
1350
|
+
waitingSpinner.fail(pc4.red("Authorization denied."));
|
|
1351
|
+
return null;
|
|
1352
|
+
}
|
|
1353
|
+
if (result.status === "expired") {
|
|
1354
|
+
waitingSpinner.fail(pc4.red("Code expired. Run login again."));
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
if (result.status === "transient") {
|
|
1358
|
+
intervalMs += 5e3;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
waitingSpinner.fail(pc4.red("Login failed"));
|
|
1363
|
+
if (error instanceof Error) console.error(pc4.red(error.message));
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
waitingSpinner.fail(pc4.red("Code expired without approval."));
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
async function performLogin(openBrowser = true, forceDevice = false) {
|
|
1371
|
+
if (forceDevice || shouldUseDeviceFlow()) {
|
|
1372
|
+
return performDeviceLogin(openBrowser);
|
|
1373
|
+
}
|
|
1179
1374
|
const spinner = ora("Preparing login...").start();
|
|
1180
1375
|
try {
|
|
1181
1376
|
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
@@ -1242,7 +1437,7 @@ async function loginCommand(options) {
|
|
|
1242
1437
|
return;
|
|
1243
1438
|
}
|
|
1244
1439
|
clearTokens();
|
|
1245
|
-
const token = await performLogin(options.browser);
|
|
1440
|
+
const token = await performLogin(options.browser, options.device ?? false);
|
|
1246
1441
|
if (!token) {
|
|
1247
1442
|
process.exit(1);
|
|
1248
1443
|
}
|