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 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 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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: new URLSearchParams({
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 exchange code for tokens");
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 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();
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
- const { codeVerifier, codeChallenge } = generatePKCE();
1182
- const state = generateState();
1183
- const callbackServer = createCallbackServer(state);
1184
- const port = await callbackServer.port;
1185
- const redirectUri = `http://localhost:${port}/callback`;
1186
- const authUrl = buildAuthorizationUrl(
1187
- baseUrl2,
1188
- CLI_CLIENT_ID,
1189
- redirectUri,
1190
- codeChallenge,
1191
- state
1192
- );
1193
- spinner.stop();
1194
- console.log("");
1195
- console.log(pc4.bold("Opening browser to log in..."));
1196
- console.log("");
1197
- if (openBrowser) {
1198
- await open(authUrl);
1199
- console.log(pc4.dim("If the browser didn't open, visit this URL:"));
1200
- } else {
1201
- console.log(pc4.dim("Open this URL in your browser:"));
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
- console.log(pc4.cyan(authUrl));
1173
+ } else {
1174
+ console.log(pc4.dim(" Open the link above in any browser to continue."));
1204
1175
  console.log("");
1205
- const waitingSpinner = ora("Waiting for login...").start();
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 { code } = await callbackServer.result;
1208
- waitingSpinner.text = "Exchanging code for tokens...";
1209
- const tokens = await exchangeCodeForTokens(
1210
- baseUrl2,
1211
- code,
1212
- codeVerifier,
1213
- redirectUri,
1214
- CLI_CLIENT_ID
1215
- );
1216
- saveTokens(tokens);
1217
- callbackServer.close();
1218
- waitingSpinner.succeed(pc4.green("Login successful!"));
1219
- return tokens.access_token;
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 as randomBytes2 } from "crypto";
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-${randomBytes2(3).toString("hex")}` })
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(() => ({}));