ctx7 0.5.0 → 0.5.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.
- package/dist/index.js +5 -221
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -932,21 +932,11 @@ import { spawn } from "child_process";
|
|
|
932
932
|
import { input, select as select2 } from "@inquirer/prompts";
|
|
933
933
|
|
|
934
934
|
// src/utils/auth.ts
|
|
935
|
-
import * as crypto from "crypto";
|
|
936
|
-
import * as http from "http";
|
|
937
935
|
import * as fs from "fs";
|
|
938
936
|
import * as path from "path";
|
|
939
937
|
import * as os from "os";
|
|
940
938
|
var CONFIG_DIR = path.join(os.homedir(), ".context7");
|
|
941
939
|
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
942
|
-
function generatePKCE() {
|
|
943
|
-
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
944
|
-
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
945
|
-
return { codeVerifier, codeChallenge };
|
|
946
|
-
}
|
|
947
|
-
function generateState() {
|
|
948
|
-
return crypto.randomBytes(16).toString("base64url");
|
|
949
|
-
}
|
|
950
940
|
function ensureConfigDir() {
|
|
951
941
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
952
942
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
@@ -1017,153 +1007,8 @@ async function getValidAccessToken() {
|
|
|
1017
1007
|
return null;
|
|
1018
1008
|
}
|
|
1019
1009
|
}
|
|
1020
|
-
var CALLBACK_PORT = 52417;
|
|
1021
|
-
function createCallbackServer(expectedState) {
|
|
1022
|
-
let resolvePort;
|
|
1023
|
-
let resolveResult;
|
|
1024
|
-
let rejectResult;
|
|
1025
|
-
let serverInstance = null;
|
|
1026
|
-
const portPromise = new Promise((resolve3) => {
|
|
1027
|
-
resolvePort = resolve3;
|
|
1028
|
-
});
|
|
1029
|
-
const resultPromise = new Promise((resolve3, reject) => {
|
|
1030
|
-
resolveResult = resolve3;
|
|
1031
|
-
rejectResult = reject;
|
|
1032
|
-
});
|
|
1033
|
-
const server = http.createServer((req, res) => {
|
|
1034
|
-
const url = new URL(req.url || "/", `http://localhost`);
|
|
1035
|
-
if (url.pathname === "/callback") {
|
|
1036
|
-
const code = url.searchParams.get("code");
|
|
1037
|
-
const state = url.searchParams.get("state");
|
|
1038
|
-
const error = url.searchParams.get("error");
|
|
1039
|
-
const errorDescription = url.searchParams.get("error_description");
|
|
1040
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1041
|
-
if (error) {
|
|
1042
|
-
res.end(errorPage(errorDescription || error));
|
|
1043
|
-
serverInstance?.close();
|
|
1044
|
-
rejectResult(new Error(errorDescription || error));
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
if (!code || !state) {
|
|
1048
|
-
res.end(errorPage("Missing authorization code or state"));
|
|
1049
|
-
serverInstance?.close();
|
|
1050
|
-
rejectResult(new Error("Missing authorization code or state"));
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (state !== expectedState) {
|
|
1054
|
-
res.end(errorPage("State mismatch - possible CSRF attack"));
|
|
1055
|
-
serverInstance?.close();
|
|
1056
|
-
rejectResult(new Error("State mismatch"));
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
res.end(successPage());
|
|
1060
|
-
serverInstance?.close();
|
|
1061
|
-
resolveResult({ code, state });
|
|
1062
|
-
} else {
|
|
1063
|
-
res.writeHead(404);
|
|
1064
|
-
res.end("Not found");
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
serverInstance = server;
|
|
1068
|
-
server.on("error", (err) => {
|
|
1069
|
-
rejectResult(err);
|
|
1070
|
-
});
|
|
1071
|
-
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
1072
|
-
resolvePort(CALLBACK_PORT);
|
|
1073
|
-
});
|
|
1074
|
-
const timeout = setTimeout(
|
|
1075
|
-
() => {
|
|
1076
|
-
server.close();
|
|
1077
|
-
rejectResult(new Error("Login timed out after 5 minutes"));
|
|
1078
|
-
},
|
|
1079
|
-
5 * 60 * 1e3
|
|
1080
|
-
);
|
|
1081
|
-
return {
|
|
1082
|
-
port: portPromise,
|
|
1083
|
-
result: resultPromise,
|
|
1084
|
-
close: () => {
|
|
1085
|
-
clearTimeout(timeout);
|
|
1086
|
-
server.close();
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
function successPage() {
|
|
1091
|
-
return `<!DOCTYPE html>
|
|
1092
|
-
<html>
|
|
1093
|
-
<head><title>Login Successful</title></head>
|
|
1094
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
1095
|
-
<div style="text-align: center; padding: 2rem;">
|
|
1096
|
-
<div style="width: 64px; height: 64px; background: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
1097
|
-
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
1098
|
-
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1099
|
-
</svg>
|
|
1100
|
-
</div>
|
|
1101
|
-
<h1 style="color: #16a34a; margin: 0 0 0.5rem;">Login Successful!</h1>
|
|
1102
|
-
<p style="color: #6b7280; margin: 0;">You can close this window and return to the terminal.</p>
|
|
1103
|
-
</div>
|
|
1104
|
-
</body>
|
|
1105
|
-
</html>`;
|
|
1106
|
-
}
|
|
1107
|
-
function escapeHtml(text) {
|
|
1108
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1109
|
-
}
|
|
1110
|
-
function errorPage(message) {
|
|
1111
|
-
const safeMessage = escapeHtml(message);
|
|
1112
|
-
return `<!DOCTYPE html>
|
|
1113
|
-
<html>
|
|
1114
|
-
<head><title>Login Failed</title></head>
|
|
1115
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
1116
|
-
<div style="text-align: center; padding: 2rem;">
|
|
1117
|
-
<div style="width: 64px; height: 64px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
1118
|
-
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
1119
|
-
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1120
|
-
</svg>
|
|
1121
|
-
</div>
|
|
1122
|
-
<h1 style="color: #dc2626; margin: 0 0 0.5rem;">Login Failed</h1>
|
|
1123
|
-
<p style="color: #6b7280; margin: 0;">${safeMessage}</p>
|
|
1124
|
-
<p style="color: #9ca3af; margin: 1rem 0 0; font-size: 0.875rem;">You can close this window.</p>
|
|
1125
|
-
</div>
|
|
1126
|
-
</body>
|
|
1127
|
-
</html>`;
|
|
1128
|
-
}
|
|
1129
|
-
async function exchangeCodeForTokens(baseUrl3, code, codeVerifier, redirectUri, clientId) {
|
|
1130
|
-
const response = await fetch(`${baseUrl3}/api/oauth/token`, {
|
|
1131
|
-
method: "POST",
|
|
1132
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1133
|
-
body: new URLSearchParams({
|
|
1134
|
-
grant_type: "authorization_code",
|
|
1135
|
-
client_id: clientId,
|
|
1136
|
-
code,
|
|
1137
|
-
code_verifier: codeVerifier,
|
|
1138
|
-
redirect_uri: redirectUri
|
|
1139
|
-
}).toString()
|
|
1140
|
-
});
|
|
1141
|
-
if (!response.ok) {
|
|
1142
|
-
const err = await response.json().catch(() => ({}));
|
|
1143
|
-
throw new Error(err.error_description || err.error || "Failed to exchange code for tokens");
|
|
1144
|
-
}
|
|
1145
|
-
return await response.json();
|
|
1146
|
-
}
|
|
1147
|
-
function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, state) {
|
|
1148
|
-
const url = new URL(`${baseUrl3}/api/oauth/authorize`);
|
|
1149
|
-
url.searchParams.set("client_id", clientId);
|
|
1150
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
1151
|
-
url.searchParams.set("code_challenge", codeChallenge);
|
|
1152
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
1153
|
-
url.searchParams.set("state", state);
|
|
1154
|
-
url.searchParams.set("scope", "profile email");
|
|
1155
|
-
url.searchParams.set("response_type", "code");
|
|
1156
|
-
return url.toString();
|
|
1157
|
-
}
|
|
1158
1010
|
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1159
1011
|
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
1012
|
async function startDeviceAuthorization(baseUrl3, clientId) {
|
|
1168
1013
|
const params = new URLSearchParams({ client_id: clientId });
|
|
1169
1014
|
try {
|
|
@@ -1236,7 +1081,7 @@ function setAuthBaseUrl(url) {
|
|
|
1236
1081
|
baseUrl2 = url;
|
|
1237
1082
|
}
|
|
1238
1083
|
function registerAuthCommands(program2) {
|
|
1239
|
-
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").
|
|
1084
|
+
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
1240
1085
|
await loginCommand(options);
|
|
1241
1086
|
});
|
|
1242
1087
|
program2.command("logout").description("Log out of Context7").action(() => {
|
|
@@ -1299,7 +1144,7 @@ async function announceIdentity(accessToken) {
|
|
|
1299
1144
|
return "Login successful!";
|
|
1300
1145
|
}
|
|
1301
1146
|
}
|
|
1302
|
-
async function
|
|
1147
|
+
async function performLogin(openBrowser = true) {
|
|
1303
1148
|
const spinner = ora("Preparing login...").start();
|
|
1304
1149
|
let authorization;
|
|
1305
1150
|
try {
|
|
@@ -1367,67 +1212,6 @@ async function performDeviceLogin(openBrowser = true) {
|
|
|
1367
1212
|
waitingSpinner.fail(pc4.red("Code expired without approval."));
|
|
1368
1213
|
return null;
|
|
1369
1214
|
}
|
|
1370
|
-
async function performLogin(openBrowser = true, forceDevice = false) {
|
|
1371
|
-
if (forceDevice || shouldUseDeviceFlow()) {
|
|
1372
|
-
return performDeviceLogin(openBrowser);
|
|
1373
|
-
}
|
|
1374
|
-
const spinner = ora("Preparing login...").start();
|
|
1375
|
-
try {
|
|
1376
|
-
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
1377
|
-
const state = generateState();
|
|
1378
|
-
const callbackServer = createCallbackServer(state);
|
|
1379
|
-
const port = await callbackServer.port;
|
|
1380
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
1381
|
-
const authUrl = buildAuthorizationUrl(
|
|
1382
|
-
baseUrl2,
|
|
1383
|
-
CLI_CLIENT_ID,
|
|
1384
|
-
redirectUri,
|
|
1385
|
-
codeChallenge,
|
|
1386
|
-
state
|
|
1387
|
-
);
|
|
1388
|
-
spinner.stop();
|
|
1389
|
-
console.log("");
|
|
1390
|
-
console.log(pc4.bold("Opening browser to log in..."));
|
|
1391
|
-
console.log("");
|
|
1392
|
-
if (openBrowser) {
|
|
1393
|
-
await open(authUrl);
|
|
1394
|
-
console.log(pc4.dim("If the browser didn't open, visit this URL:"));
|
|
1395
|
-
} else {
|
|
1396
|
-
console.log(pc4.dim("Open this URL in your browser:"));
|
|
1397
|
-
}
|
|
1398
|
-
console.log(pc4.cyan(authUrl));
|
|
1399
|
-
console.log("");
|
|
1400
|
-
const waitingSpinner = ora("Waiting for login...").start();
|
|
1401
|
-
try {
|
|
1402
|
-
const { code } = await callbackServer.result;
|
|
1403
|
-
waitingSpinner.text = "Exchanging code for tokens...";
|
|
1404
|
-
const tokens = await exchangeCodeForTokens(
|
|
1405
|
-
baseUrl2,
|
|
1406
|
-
code,
|
|
1407
|
-
codeVerifier,
|
|
1408
|
-
redirectUri,
|
|
1409
|
-
CLI_CLIENT_ID
|
|
1410
|
-
);
|
|
1411
|
-
saveTokens(tokens);
|
|
1412
|
-
callbackServer.close();
|
|
1413
|
-
waitingSpinner.succeed(pc4.green("Login successful!"));
|
|
1414
|
-
return tokens.access_token;
|
|
1415
|
-
} catch (error) {
|
|
1416
|
-
callbackServer.close();
|
|
1417
|
-
waitingSpinner.fail(pc4.red("Login failed"));
|
|
1418
|
-
if (error instanceof Error) {
|
|
1419
|
-
console.error(pc4.red(error.message));
|
|
1420
|
-
}
|
|
1421
|
-
return null;
|
|
1422
|
-
}
|
|
1423
|
-
} catch (error) {
|
|
1424
|
-
spinner.fail(pc4.red("Login failed"));
|
|
1425
|
-
if (error instanceof Error) {
|
|
1426
|
-
console.error(pc4.red(error.message));
|
|
1427
|
-
}
|
|
1428
|
-
return null;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
1215
|
async function loginCommand(options) {
|
|
1432
1216
|
trackEvent("command", { name: "login" });
|
|
1433
1217
|
const existingToken = await getValidAccessToken();
|
|
@@ -1437,7 +1221,7 @@ async function loginCommand(options) {
|
|
|
1437
1221
|
return;
|
|
1438
1222
|
}
|
|
1439
1223
|
clearTokens();
|
|
1440
|
-
const token = await performLogin(options.browser
|
|
1224
|
+
const token = await performLogin(options.browser);
|
|
1441
1225
|
if (!token) {
|
|
1442
1226
|
process.exit(1);
|
|
1443
1227
|
}
|
|
@@ -2837,7 +2621,7 @@ import ora4 from "ora";
|
|
|
2837
2621
|
import { select as select3 } from "@inquirer/prompts";
|
|
2838
2622
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2839
2623
|
import { dirname as dirname6, join as join8 } from "path";
|
|
2840
|
-
import { randomBytes
|
|
2624
|
+
import { randomBytes } from "crypto";
|
|
2841
2625
|
|
|
2842
2626
|
// src/setup/agents.ts
|
|
2843
2627
|
import { access as access2 } from "fs/promises";
|
|
@@ -3432,7 +3216,7 @@ async function authenticateAndGenerateKey() {
|
|
|
3432
3216
|
Authorization: `Bearer ${accessToken}`,
|
|
3433
3217
|
"Content-Type": "application/json"
|
|
3434
3218
|
},
|
|
3435
|
-
body: JSON.stringify({ name: `ctx7-cli-${
|
|
3219
|
+
body: JSON.stringify({ name: `ctx7-cli-${randomBytes(3).toString("hex")}` })
|
|
3436
3220
|
});
|
|
3437
3221
|
if (!response.ok) {
|
|
3438
3222
|
const err = await response.json().catch(() => ({}));
|