ctx7 0.4.5 → 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 +166 -187
- package/dist/index.js.map +1 -1
- package/package.json +2 -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,149 +1007,75 @@ async function getValidAccessToken() {
|
|
|
1017
1007
|
return null;
|
|
1018
1008
|
}
|
|
1019
1009
|
}
|
|
1020
|
-
var
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
const
|
|
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`, {
|
|
1010
|
+
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1011
|
+
var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
1012
|
+
async function startDeviceAuthorization(baseUrl3, clientId) {
|
|
1013
|
+
const params = new URLSearchParams({ client_id: clientId });
|
|
1014
|
+
try {
|
|
1015
|
+
const hostname2 = os.hostname();
|
|
1016
|
+
if (hostname2) params.set("hostname", hostname2);
|
|
1017
|
+
} catch {
|
|
1018
|
+
}
|
|
1019
|
+
const response = await fetch(`${baseUrl3}/api/oauth/device/code`, {
|
|
1131
1020
|
method: "POST",
|
|
1132
1021
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1133
|
-
body:
|
|
1134
|
-
grant_type: "authorization_code",
|
|
1135
|
-
client_id: clientId,
|
|
1136
|
-
code,
|
|
1137
|
-
code_verifier: codeVerifier,
|
|
1138
|
-
redirect_uri: redirectUri
|
|
1139
|
-
}).toString()
|
|
1022
|
+
body: params.toString()
|
|
1140
1023
|
});
|
|
1141
1024
|
if (!response.ok) {
|
|
1142
1025
|
const err = await response.json().catch(() => ({}));
|
|
1143
|
-
throw new Error(err.error_description || err.error || "Failed to
|
|
1026
|
+
throw new Error(err.error_description || err.error || "Failed to start device authorization");
|
|
1144
1027
|
}
|
|
1145
1028
|
return await response.json();
|
|
1146
1029
|
}
|
|
1147
|
-
function
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1030
|
+
async function pollDeviceToken(baseUrl3, clientId, deviceCode) {
|
|
1031
|
+
let response;
|
|
1032
|
+
try {
|
|
1033
|
+
response = await fetch(`${baseUrl3}/api/oauth/device/token`, {
|
|
1034
|
+
method: "POST",
|
|
1035
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1036
|
+
body: new URLSearchParams({
|
|
1037
|
+
grant_type: DEVICE_CODE_GRANT,
|
|
1038
|
+
device_code: deviceCode,
|
|
1039
|
+
client_id: clientId
|
|
1040
|
+
}).toString()
|
|
1041
|
+
});
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
return {
|
|
1044
|
+
status: "transient",
|
|
1045
|
+
errorMessage: error instanceof Error ? error.message : "network error"
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
if (response.ok) {
|
|
1049
|
+
const tokens = await response.json();
|
|
1050
|
+
return { status: "approved", tokens };
|
|
1051
|
+
}
|
|
1052
|
+
if (response.status >= 500) {
|
|
1053
|
+
const err2 = await response.json().catch(() => ({}));
|
|
1054
|
+
return {
|
|
1055
|
+
status: "transient",
|
|
1056
|
+
errorMessage: err2.error_description || err2.error || `HTTP ${response.status}`
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const err = await response.json().catch(() => ({}));
|
|
1060
|
+
switch (err.error) {
|
|
1061
|
+
case "authorization_pending":
|
|
1062
|
+
return { status: "pending" };
|
|
1063
|
+
case "slow_down":
|
|
1064
|
+
return { status: "slow_down" };
|
|
1065
|
+
case "access_denied":
|
|
1066
|
+
return { status: "denied" };
|
|
1067
|
+
case "expired_token":
|
|
1068
|
+
return { status: "expired" };
|
|
1069
|
+
default:
|
|
1070
|
+
throw new Error(err.error_description || err.error || "Device token poll failed");
|
|
1071
|
+
}
|
|
1157
1072
|
}
|
|
1158
1073
|
|
|
1159
1074
|
// src/commands/auth.ts
|
|
1160
1075
|
import pc4 from "picocolors";
|
|
1161
1076
|
import ora from "ora";
|
|
1162
1077
|
import open from "open";
|
|
1078
|
+
import boxen from "boxen";
|
|
1163
1079
|
var baseUrl2 = "https://context7.com";
|
|
1164
1080
|
function setAuthBaseUrl(url) {
|
|
1165
1081
|
baseUrl2 = url;
|
|
@@ -1175,63 +1091,126 @@ function registerAuthCommands(program2) {
|
|
|
1175
1091
|
await whoamiCommand();
|
|
1176
1092
|
});
|
|
1177
1093
|
}
|
|
1094
|
+
function renderDeviceCodeBox(userCode, verificationUri, verificationUriComplete) {
|
|
1095
|
+
const codeLine = `${pc4.dim("Your one-time code:")}
|
|
1096
|
+
|
|
1097
|
+
${pc4.green(pc4.bold(userCode))}`;
|
|
1098
|
+
const linkLine = verificationUriComplete ? `${pc4.dim("Open this link to approve:")}
|
|
1099
|
+
${pc4.cyan(verificationUriComplete)}
|
|
1100
|
+
|
|
1101
|
+
${pc4.dim("Or visit")} ${pc4.cyan(verificationUri)} ${pc4.dim("and enter the code above.")}` : `${pc4.dim("Visit:")} ${pc4.cyan(verificationUri)}`;
|
|
1102
|
+
return boxen(`${codeLine}
|
|
1103
|
+
|
|
1104
|
+
${linkLine}`, {
|
|
1105
|
+
title: "Sign in to Context7",
|
|
1106
|
+
titleAlignment: "left",
|
|
1107
|
+
padding: 1,
|
|
1108
|
+
margin: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
1109
|
+
borderStyle: "round",
|
|
1110
|
+
borderColor: "gray"
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
function waitForEnter(prompt) {
|
|
1114
|
+
if (!process.stdin.isTTY) return Promise.resolve();
|
|
1115
|
+
return new Promise((resolve3) => {
|
|
1116
|
+
process.stdout.write(` ${pc4.dim(prompt)} `);
|
|
1117
|
+
const onData = (chunk) => {
|
|
1118
|
+
if (chunk[0] === 3) {
|
|
1119
|
+
process.stdin.removeListener("data", onData);
|
|
1120
|
+
process.stdin.setRawMode?.(false);
|
|
1121
|
+
process.stdin.pause();
|
|
1122
|
+
process.stdout.write("\n");
|
|
1123
|
+
process.exit(130);
|
|
1124
|
+
}
|
|
1125
|
+
process.stdin.removeListener("data", onData);
|
|
1126
|
+
process.stdin.setRawMode?.(false);
|
|
1127
|
+
process.stdin.pause();
|
|
1128
|
+
process.stdout.write("\n");
|
|
1129
|
+
resolve3();
|
|
1130
|
+
};
|
|
1131
|
+
process.stdin.setRawMode?.(true);
|
|
1132
|
+
process.stdin.resume();
|
|
1133
|
+
process.stdin.on("data", onData);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
async function announceIdentity(accessToken) {
|
|
1137
|
+
try {
|
|
1138
|
+
const whoami = await fetchWhoami(accessToken);
|
|
1139
|
+
const name = whoami.email || whoami.name;
|
|
1140
|
+
if (!name) return "Login successful!";
|
|
1141
|
+
const team = whoami.teamspace?.name;
|
|
1142
|
+
return team ? `Logged in as ${pc4.bold(name)} ${pc4.dim(`(${team})`)}` : `Logged in as ${pc4.bold(name)}`;
|
|
1143
|
+
} catch {
|
|
1144
|
+
return "Login successful!";
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1178
1147
|
async function performLogin(openBrowser = true) {
|
|
1179
1148
|
const spinner = ora("Preparing login...").start();
|
|
1149
|
+
let authorization;
|
|
1180
1150
|
try {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
console.log(pc4.dim(
|
|
1151
|
+
authorization = await startDeviceAuthorization(baseUrl2, CLI_CLIENT_ID);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
spinner.fail(pc4.red("Login failed"));
|
|
1154
|
+
if (error instanceof Error) console.error(pc4.red(error.message));
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
spinner.stop();
|
|
1158
|
+
console.log(
|
|
1159
|
+
renderDeviceCodeBox(
|
|
1160
|
+
authorization.user_code,
|
|
1161
|
+
authorization.verification_uri,
|
|
1162
|
+
authorization.verification_uri_complete
|
|
1163
|
+
)
|
|
1164
|
+
);
|
|
1165
|
+
const target = authorization.verification_uri_complete ?? authorization.verification_uri;
|
|
1166
|
+
if (openBrowser) {
|
|
1167
|
+
await waitForEnter("Press Enter to open the browser, or Ctrl-C to quit...");
|
|
1168
|
+
try {
|
|
1169
|
+
await open(target);
|
|
1170
|
+
} catch {
|
|
1171
|
+
console.log(pc4.dim(` Couldn't open a browser \u2014 visit the link above manually.`));
|
|
1202
1172
|
}
|
|
1203
|
-
|
|
1173
|
+
} else {
|
|
1174
|
+
console.log(pc4.dim(" Open the link above in any browser to continue."));
|
|
1204
1175
|
console.log("");
|
|
1205
|
-
|
|
1176
|
+
}
|
|
1177
|
+
const waitingSpinner = ora({ text: "Waiting for authorization...", indent: 2 }).start();
|
|
1178
|
+
const deadline = Date.now() + authorization.expires_in * 1e3;
|
|
1179
|
+
let intervalMs = (authorization.interval ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1e3;
|
|
1180
|
+
while (Date.now() < deadline) {
|
|
1181
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
1206
1182
|
try {
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1183
|
+
const result = await pollDeviceToken(baseUrl2, CLI_CLIENT_ID, authorization.device_code);
|
|
1184
|
+
if (result.status === "approved" && result.tokens) {
|
|
1185
|
+
saveTokens(result.tokens);
|
|
1186
|
+
const successText = await announceIdentity(result.tokens.access_token);
|
|
1187
|
+
waitingSpinner.succeed(pc4.green(successText));
|
|
1188
|
+
return result.tokens.access_token;
|
|
1189
|
+
}
|
|
1190
|
+
if (result.status === "slow_down") {
|
|
1191
|
+
intervalMs += 5e3;
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (result.status === "denied") {
|
|
1195
|
+
waitingSpinner.fail(pc4.red("Authorization denied."));
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
if (result.status === "expired") {
|
|
1199
|
+
waitingSpinner.fail(pc4.red("Code expired. Run login again."));
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
if (result.status === "transient") {
|
|
1203
|
+
intervalMs += 5e3;
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1220
1206
|
} catch (error) {
|
|
1221
|
-
callbackServer.close();
|
|
1222
1207
|
waitingSpinner.fail(pc4.red("Login failed"));
|
|
1223
|
-
if (error instanceof Error)
|
|
1224
|
-
console.error(pc4.red(error.message));
|
|
1225
|
-
}
|
|
1208
|
+
if (error instanceof Error) console.error(pc4.red(error.message));
|
|
1226
1209
|
return null;
|
|
1227
1210
|
}
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
spinner.fail(pc4.red("Login failed"));
|
|
1230
|
-
if (error instanceof Error) {
|
|
1231
|
-
console.error(pc4.red(error.message));
|
|
1232
|
-
}
|
|
1233
|
-
return null;
|
|
1234
1211
|
}
|
|
1212
|
+
waitingSpinner.fail(pc4.red("Code expired without approval."));
|
|
1213
|
+
return null;
|
|
1235
1214
|
}
|
|
1236
1215
|
async function loginCommand(options) {
|
|
1237
1216
|
trackEvent("command", { name: "login" });
|
|
@@ -2642,7 +2621,7 @@ import ora4 from "ora";
|
|
|
2642
2621
|
import { select as select3 } from "@inquirer/prompts";
|
|
2643
2622
|
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2644
2623
|
import { dirname as dirname6, join as join8 } from "path";
|
|
2645
|
-
import { randomBytes
|
|
2624
|
+
import { randomBytes } from "crypto";
|
|
2646
2625
|
|
|
2647
2626
|
// src/setup/agents.ts
|
|
2648
2627
|
import { access as access2 } from "fs/promises";
|
|
@@ -3237,7 +3216,7 @@ async function authenticateAndGenerateKey() {
|
|
|
3237
3216
|
Authorization: `Bearer ${accessToken}`,
|
|
3238
3217
|
"Content-Type": "application/json"
|
|
3239
3218
|
},
|
|
3240
|
-
body: JSON.stringify({ name: `ctx7-cli-${
|
|
3219
|
+
body: JSON.stringify({ name: `ctx7-cli-${randomBytes(3).toString("hex")}` })
|
|
3241
3220
|
});
|
|
3242
3221
|
if (!response.ok) {
|
|
3243
3222
|
const err = await response.json().catch(() => ({}));
|