webcrawl 1.3.5 → 1.3.6

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/cli.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface Deps {
10
10
  fetch?: typeof fetch;
11
11
  /** Config file path override (tests). */
12
12
  configFile?: string;
13
+ /** Browser opener override (tests). */
14
+ openUrl?: (url: string) => void;
13
15
  /** Home directory override for `webcrawl setup` tests. */
14
16
  homeDir?: string;
15
17
  /** Working directory override for `webcrawl setup` tests. */
package/dist/cli.js CHANGED
@@ -112,7 +112,7 @@ function openBrowserUrl(url) {
112
112
  // ignore
113
113
  }
114
114
  }
115
- async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
115
+ async function runWebOAuthLogin(baseUrl, fetchImpl, io, openUrl = openBrowserUrl) {
116
116
  const verifier = crypto.randomBytes(32).toString("base64url");
117
117
  const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
118
118
  const state = crypto.randomBytes(16).toString("base64url");
@@ -153,7 +153,8 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
153
153
  "",
154
154
  "Waiting for browser authorization...",
155
155
  ].join("\n") + "\n");
156
- openBrowserUrl(authUrl);
156
+ openUrl(authUrl);
157
+ let handled = false;
157
158
  server.on("request", async (req, res) => {
158
159
  try {
159
160
  const u = new URL(req.url || "/", redirectUri);
@@ -163,14 +164,29 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
163
164
  }
164
165
  const code = u.searchParams.get("code");
165
166
  const returnedState = u.searchParams.get("state");
166
- if (!code || returnedState !== state) {
167
+ if (handled) {
168
+ // Browser retry / prefetch of the same callback: the code is single-use, so never exchange it twice.
169
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
170
+ res.end("<h3>Already received. You can close this tab and return to your terminal.</h3>");
171
+ return;
172
+ }
173
+ const oauthError = u.searchParams.get("error");
174
+ if (oauthError && returnedState === state) {
175
+ handled = true;
167
176
  res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
168
- res.end("<h3>Authorization failed or state mismatch. You can close this tab.</h3>");
177
+ res.end("<h3>Authorization was cancelled. You can close this tab.</h3>");
169
178
  clearTimeout(timeout);
170
179
  server.close();
171
- reject(new Error("OAuth callback missing code or state mismatch."));
180
+ reject(new Error(`OAuth authorization failed: ${oauthError}.`));
181
+ return;
182
+ }
183
+ if (!code || returnedState !== state) {
184
+ // Stray or forged request: reject it but keep waiting for the real callback.
185
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
186
+ res.end("<h3>Authorization failed or state mismatch.</h3>");
172
187
  return;
173
188
  }
189
+ handled = true;
174
190
  const tokenRes = await fetchImpl(`${baseUrl}/oauth/token`, {
175
191
  method: "POST",
176
192
  headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
@@ -183,7 +199,11 @@ async function runWebOAuthLogin(baseUrl, fetchImpl, io) {
183
199
  }).toString(),
184
200
  });
185
201
  if (!tokenRes.ok) {
186
- throw new Error(`OAuth token exchange failed (HTTP ${tokenRes.status}).`);
202
+ const detail = (await tokenRes.json().catch(() => ({})));
203
+ const reason = detail.error_description || detail.error;
204
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
205
+ res.end("<h3>Webcrawl sign-in failed. Return to your terminal for details.</h3>");
206
+ throw new Error(`OAuth token exchange failed (HTTP ${tokenRes.status}${reason ? `: ${reason}` : ""}). Run \`webcrawl login\` again, or use \`webcrawl login --email <you@domain.com>\`.`);
187
207
  }
188
208
  const tokenData = (await tokenRes.json());
189
209
  let finalKey = tokenData.access_token;
@@ -449,7 +469,7 @@ export async function main(argv, io, deps = {}) {
449
469
  });
450
470
  // 1. Browser OAuth PKCE redirect (default for `webcrawl login`; `--web` kept for compatibility)
451
471
  if (v.web || (!v.email && !v.key)) {
452
- const webRes = await runWebOAuthLogin(cfg.baseUrl, deps.fetch ?? globalThis.fetch, io);
472
+ const webRes = await runWebOAuthLogin(cfg.baseUrl, deps.fetch ?? globalThis.fetch, io, deps.openUrl);
453
473
  const existing = readConfigFile(file);
454
474
  writeConfigFile({ ...existing, apiKey: webRes.apiKey, ...(v.url ? { baseUrl: v.url } : {}) }, file);
455
475
  const payload = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcrawl",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "Webcrawl Anything CLI & Core Engine — crawl, search, and extract structured evidence from any website, SPA, PDF, Office file, YouTube video, GitHub repo, ArXiv paper, or CVE",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -127,8 +127,8 @@
127
127
  "@ai-sdk/gateway": "^4.0.123",
128
128
  "@firecrawl/pdf-inspector": "^1.25.2",
129
129
  "@vercel/sandbox": "^1.0.0",
130
- "@webcrawl/client": "^1.3.5",
131
- "@webcrawl/curl-impersonate": "^1.3.5",
130
+ "@webcrawl/client": "^1.3.6",
131
+ "@webcrawl/curl-impersonate": "^1.3.6",
132
132
  "ai": "^7.0.150",
133
133
  "impit": "^0.14.7",
134
134
  "mammoth": "^1.13.0",