From 1a9d29a9bcffdc7b89a2e43c0b80f676855a5aa7 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:01:14 -0300 Subject: [PATCH] fix(providers): register Firecrawl and Jina Reader API-key validators (#4401) (#4412) --- config/quality/file-size-baseline.json | 3 +- src/lib/providers/validation.ts | 22 ++++++ .../provider-validation-webfetch-4401.test.ts | 67 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/unit/provider-validation-webfetch-4401.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 36e4ad8b15..ae6ace07c9 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,5 +1,6 @@ { "_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", + "_rebaseline_2026_06_20_4401_webfetch_validators": "PR #4401 own growth: src/lib/providers/validation.ts 4428->4450 (+22 = two new API-key validators — Firecrawl + Jina Reader — each a cohesive provider validator branch mirroring the existing ones; 401/403->invalid else->valid). Not extractable (it IS the per-provider validator list). Covered by tests/unit/provider-validation-webfetch-4401.test.ts.", "_rebaseline_2026_06_20_4380_parse_once": "PR #4380 own growth: src/sse/handlers/chat.ts 1486->1491 (+5 = thread the once-parsed request body from the route guard into handleChat, replacing the duplicate re-parse). The reusable body accessor lives in the new src/sse/handlers/requestBody.ts (2586 (sweep +19 NAMED_OPENAI_STYLE + #4313 +3 openadapter/dit/tokenrouter = uniao no Set, regioes disjuntas). providers.ts 3198->3242 (sweep 6 dead-provider marks + #4313 3 novas entries APIKEY_PROVIDERS). combos/page.tsx 4350->4385 (#4313 #3266 allowlist UI) e providers/page.tsx 1925->1927 (#4313 #4240 serviceKind state) — intocados pelo sweep, crescimento puro do #4313. Mesmo padrao release-volatil de medir-no-merge dos baselines de complexity/zizmor deste lote.", "_rebaseline_2026_06_19_provider_sweep_merge_reconcile": "provider-model-sweep PR merge into release/v3.8.30: the sweep's route.ts growth (NAMED_OPENAI_STYLE_PROVIDERS +19 entries, base 2538->2564) and #4259's cloudflare parseResponse (2538->2554) are disjoint regions that both land, so the merged route.ts frozen value is reconciled to its measured size here. constants/providers.ts carries the sweep's 6 dead-provider deprecation marks on top of release. chatCore.ts stays at #4266's 5128 (sweep does not touch it).", @@ -168,7 +169,7 @@ "src/lib/evals/evalRunner.ts": 961, "src/lib/memory/retrieval.ts": 1171, "src/lib/modelsDevSync.ts": 934, - "src/lib/providers/validation.ts": 4428, + "src/lib/providers/validation.ts": 4450, "src/lib/tailscaleTunnel.ts": 1202, "src/lib/usage/callLogs.ts": 975, "src/lib/usage/providerLimits.ts": 949, diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 24ccc96650..73ba194817 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -2678,6 +2678,28 @@ const SEARCH_VALIDATOR_CONFIGS: Record< }, }; }, + // ── Web-fetch providers (#4401) ── + // firecrawl / jina-reader were added as webFetch-kind providers in #2645 with their + // own executors but no validator, so the dashboard "Validate" step returned + // "Provider validation not supported" and accounts could not be added through the UI. + // Probe each provider's real fetch endpoint with the same Bearer auth the executor + // uses; validateSearchProvider maps 200/<500 → valid, 401/403 → invalid key, + // >=500 → failure (a credit-exhausted / rate-limited key still validates). + firecrawl: (apiKey) => ({ + url: "https://api.firecrawl.dev/v1/scrape", + init: { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` }, + body: JSON.stringify({ url: "https://example.com", formats: ["markdown"] }), + }, + }), + "jina-reader": (apiKey) => ({ + url: "https://r.jina.ai/https://example.com", + init: { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}` }, + }, + }), }; // See open-sse/executors/muse-spark-web.ts for the rationale: Meta migrated diff --git a/tests/unit/provider-validation-webfetch-4401.test.ts b/tests/unit/provider-validation-webfetch-4401.test.ts new file mode 100644 index 0000000000..ec89d68e7b --- /dev/null +++ b/tests/unit/provider-validation-webfetch-4401.test.ts @@ -0,0 +1,67 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +// #4401: Firecrawl and Jina Reader were added as webFetch providers in #2645 with +// their own executors, but no API-key validator was registered — so adding an account +// through the dashboard failed with "Provider validation not supported". These tests +// pin the validator dispatch (firecrawl → POST api.firecrawl.dev/v1/scrape with Bearer; +// jina-reader → GET r.jina.ai/ with Bearer) and the auth-failure mapping. + +const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); + +const originalFetch = globalThis.fetch; + +test.afterEach(() => { + globalThis.fetch = originalFetch; +}); + +function headerValue(init: RequestInit | undefined, name: string): string | undefined { + const headers = (init?.headers || {}) as Record; + return headers[name]; +} + +test("#4401 firecrawl validator probes the scrape endpoint with Bearer auth and accepts a 200", async () => { + const calls: { url: string; init: RequestInit }[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init }); + return new Response(JSON.stringify({ success: true, data: {} }), { status: 200 }); + }; + + const result = await validateProviderApiKey({ provider: "firecrawl", apiKey: "fc-test-key" }); + + assert.equal(result.valid, true); + assert.equal(result.unsupported ?? false, false); + assert.equal(calls.length, 1); + assert.equal(calls[0].url, "https://api.firecrawl.dev/v1/scrape"); + assert.equal(calls[0].init.method, "POST"); + assert.equal(headerValue(calls[0].init, "Authorization"), "Bearer fc-test-key"); +}); + +test("#4401 jina-reader validator probes r.jina.ai with Bearer auth and accepts a 200", async () => { + const calls: { url: string; init: RequestInit }[] = []; + globalThis.fetch = async (url, init = {}) => { + calls.push({ url: String(url), init }); + return new Response("# Example\n", { status: 200 }); + }; + + const result = await validateProviderApiKey({ provider: "jina-reader", apiKey: "jina-test-key" }); + + assert.equal(result.valid, true); + assert.equal(result.unsupported ?? false, false); + assert.equal(calls.length, 1); + assert.match(calls[0].url, /^https:\/\/r\.jina\.ai\//); + assert.equal(headerValue(calls[0].init, "Authorization"), "Bearer jina-test-key"); +}); + +test("#4401 webFetch validators map 401/403 to an invalid-key error, not 'not supported'", async () => { + globalThis.fetch = async () => + new Response(JSON.stringify({ error: "unauthorized" }), { status: 401 }); + + const firecrawl = await validateProviderApiKey({ provider: "firecrawl", apiKey: "bad" }); + const jina = await validateProviderApiKey({ provider: "jina-reader", apiKey: "bad" }); + + assert.equal(firecrawl.valid, false); + assert.equal(firecrawl.error, "Invalid API key"); + assert.equal(jina.valid, false); + assert.equal(jina.error, "Invalid API key"); +});