chore(release): finalize v3.8.30 — complete CHANGELOG + CodeQL freeWebSearch hardening + docs reconcile

- CHANGELOG: complete the [3.8.30] section to 1:1 coverage of all 70 commits
  since v3.8.29 (Features/Changed/Fixed/Tests/Maintenance/Security/Dependencies).
- security: cherry-pick the DDG-lite scraper sanitization hardening from #4356
  (closes 4 HIGH CodeQL alerts: js/double-escaping, js/incomplete-multi-character-
  sanitization, js/incomplete-url-substring-sanitization x2).
- docs: ANTHROPIC_AUTH_TOKEN (not the bare AUTH_TOKEN shorthand) in the Claude Code
  guide; allowlist GEMINI_API_KEY/GOOGLE_GEMINI_BASE_URL (Gemini CLI vars) in the
  fabricated-docs checker — fixes the docs-sync-strict gate.
This commit is contained in:
diegosouzapw
2026-06-20 06:43:50 -03:00
parent 1a1ef10009
commit ab1dfddcc8
6 changed files with 132 additions and 16 deletions

View File

@@ -4,9 +4,7 @@
---
## [3.8.30] — TBD
_In development — bullets added per PR; finalized at release._
## [3.8.30] — 2026-06-20
### ✨ New Features
@@ -15,6 +13,13 @@ _In development — bullets added per PR; finalized at release._
- **feat(providers): add OpenAdapter, dit.ai and TokenRouter as OpenAI-compatible providers** — three community-requested OpenAI-compatible aggregators now register as standard named OpenAI-style providers with live `/v1/models` discovery (the zenmux pattern), falling back to a seeded catalog when the upstream list is unavailable: **OpenAdapter** (`https://api.openadapter.in/v1`, free tier, 70+ open-source models — [#4239](https://github.com/diegosouzapw/OmniRoute/issues/4239)), **dit.ai** (`https://api.dit.ai/v1`, dynamic-pricing router/gateway — [#4155](https://github.com/diegosouzapw/OmniRoute/issues/4155)), and **TokenRouter** (`https://api.tokenrouter.com/v1`, free MiniMax model — [#3841](https://github.com/diegosouzapw/OmniRoute/issues/3841), thanks @FerLuisxd). No custom executor/translator — default OpenAI passthrough.
- **feat(api): `x-omniroute-no-memory` request header — per-request opt-out of memory/skills injection** — clients that manage their own context (e.g. their own RAG/memory) can send `x-omniroute-no-memory: true` (mirrors the existing `x-omniroute-no-cache` convention) to skip the gateway injecting up to `memorySettings.maxTokens` (~2k) tokens of memory **and** skills context into that chat request — avoiding the token/cost inflation it otherwise adds on every call. Absent the header, behavior is unchanged. (PRD-2026-06-19-no-memory-header)
- **feat(dashboard): MITM tool card lists the exact hosts-file entries to add manually** — the CLI-tools MITM card's "How it works" section now lists the full set of `127.0.0.1 <host>` lines for the selected tool (sourced from the canonical MITM target registry) instead of a single example domain. Users on locked-down machines — where the automatic, sudo-gated hosts-file edit isn't available — can now copy every required entry by hand. (thanks @mrcyclo)
- **feat(cli): `omniroute launch-codex` + `setup-codex` — run/configure the Codex CLI against OmniRoute** — a launcher and setup command that point the Codex CLI at an OmniRoute endpoint (remote-mode aware). ([#4270](https://github.com/diegosouzapw/OmniRoute/pull/4270))
- **feat(cli): Claude Code launcher + setup — remote mode + profiles**`omniroute launch`/`setup` for Claude Code with remote-mode support and named connection profiles. ([#4274](https://github.com/diegosouzapw/OmniRoute/pull/4274))
- **feat(cli): OpenCode setup — OpenAI-compatible provider + remote-aware plugin**`setup-opencode` registers OmniRoute as an OpenAI-compatible provider for OpenCode and installs a remote-aware plugin. ([#4277](https://github.com/diegosouzapw/OmniRoute/pull/4277))
- **feat(cli): one-command setup for popular AI coding tools** — new `setup-*` commands that configure each tool to talk to OmniRoute: **Cline** ([#4280](https://github.com/diegosouzapw/OmniRoute/pull/4280)), **Kilo Code** ([#4284](https://github.com/diegosouzapw/OmniRoute/pull/4284)), **Continue** ([#4289](https://github.com/diegosouzapw/OmniRoute/pull/4289)), **Cursor** ([#4291](https://github.com/diegosouzapw/OmniRoute/pull/4291)), **Roo Code** ([#4292](https://github.com/diegosouzapw/OmniRoute/pull/4292)), **Crush** ([#4298](https://github.com/diegosouzapw/OmniRoute/pull/4298)), **Goose** ([#4300](https://github.com/diegosouzapw/OmniRoute/pull/4300)), **Qwen Code** ([#4301](https://github.com/diegosouzapw/OmniRoute/pull/4301)), **Aider** ([#4302](https://github.com/diegosouzapw/OmniRoute/pull/4302)) and the **Gemini CLI** (native `/v1beta`) ([#4303](https://github.com/diegosouzapw/OmniRoute/pull/4303)).
- **feat(providers): provider model sweep — live discovery, refreshed catalogs, dead-provider cleanup** — a broad sweep that enables live `/v1/models` discovery for more OpenAI-style providers (the zenmux pattern), refreshes the seeded catalogs with current models, and marks dead providers `deprecated`. ([#4324](https://github.com/diegosouzapw/OmniRoute/pull/4324))
- **feat(mitm): translate Antigravity cloudcode end-to-end (Gap B)** — the MITM decrypt path now translates Antigravity `cloudcode` traffic end-to-end. ([#4299](https://github.com/diegosouzapw/OmniRoute/pull/4299))
- **feat(keys): per-key USD usage quota controls** — an API key can now carry a USD spend quota that caps its usage once the threshold is reached. ([#4327](https://github.com/diegosouzapw/OmniRoute/pull/4327) — thanks @Witroch4)
### 🔧 Changed
@@ -39,6 +44,54 @@ _In development — bullets added per PR; finalized at release._
- **fix(oauth): GitHub Copilot token refresh now sends the public client_id** — the `github` provider config never carried a `clientId`, so GitHub OAuth `refresh_token` exchanges either omitted `client_id` or sent the literal string `undefined` (and a bogus `client_secret=undefined`), which GitHub rejects — leaving a Copilot connection stuck once its short-lived token expired and the long-lived refresh path was needed. The provider now resolves its public device-flow `client_id` from the embedded public credential and omits `client_secret` entirely (GitHub's Copilot app is a public client with no secret). (thanks @baslr)
- **fix(translator): a tool property named `pattern` survives Gemini/Antigravity schema sanitization** — the Gemini schema sanitizer strips JSON-Schema constraint keywords Gemini rejects (`pattern`, `minLength`, …) at every nesting level, but it also deleted any tool **property** literally *named* one of those keywords. glob/grep tools declare a property called `pattern`, so on `ag/*` (Antigravity) backends that argument (and its `required` entry) was silently dropped, breaking the tools. Keyword stripping is now position-aware: it only removes constraint keywords at the schema-node level and never against the user-defined names inside a `properties` map. A genuine string-level `pattern` *constraint* is still stripped. (thanks @youthanh)
- **fix(translator): MCP `namespace` tools flatten to individual functions on the Responses→Chat path** — when a Codex CLI client routes a Responses-API request to a non-Codex backend (e.g. `kr/claude-opus-4.7`), each MCP server is declared as a `namespace` tool (`{ type:"namespace", name, tools:[…] }`). The Responses→Chat translator had no `namespace` branch, so the whole group collapsed into a single empty-schema function named `mcp__<server>__` and every MCP call returned `unsupported call: mcp__<server>__`, breaking all MCP-based workflows (context7, codegraph, custom MCPs) for that combination. The translator now expands a namespace into one Chat function per sub-tool (preserving each sub-tool's name and parameters); an empty namespace yields no tools instead of a broken placeholder. The native Codex passthrough path was already correct. (thanks @V13t4nh)
- **fix(cli): the active remote-context credential wins over an ambient `OMNIROUTE_API_KEY`** — when a remote context is selected, its scoped access token now takes precedence over an `OMNIROUTE_API_KEY` present in the environment, so the connected remote is targeted as expected. ([#4364](https://github.com/diegosouzapw/OmniRoute/pull/4364))
- **fix(cli): wire the `contexts` command into the CLI program** — the `omniroute contexts` command (list/switch saved remote contexts) was implemented but never registered, so it was unreachable; it is now wired into the CLI program. ([#4369](https://github.com/diegosouzapw/OmniRoute/pull/4369))
- **fix(mitm): mask bare `Bearer <token>` header values in the Traffic Inspector** — the inspector now redacts bare `Authorization: Bearer …` values so tokens don't leak into captured traffic. ([#4358](https://github.com/diegosouzapw/OmniRoute/pull/4358))
- **fix(pricing): price the `gpt-5.x-pro` OpenAI models + align the opencode-go discovery test** — adds pricing for the gpt-5.x-pro models so cost telemetry reports a real cost instead of zero. ([#4355](https://github.com/diegosouzapw/OmniRoute/pull/4355))
- **fix(sse): release the reader and cancel the stream on abort/error (no more Undici pool socket leak)** — on abort or a mid-stream error the response reader is released and the stream cancelled, preventing leaked pooled sockets that degraded later requests. ([#4309](https://github.com/diegosouzapw/OmniRoute/pull/4309) — thanks @Ardem2025)
- **fix(kiro): emit an early role-only start chunk to release the stream-readiness gate** — Kiro streams now send an initial role-only chunk so the stream-readiness gate releases promptly instead of stalling. ([#4311](https://github.com/diegosouzapw/OmniRoute/pull/4311) — thanks @artickc)
- **fix(dashboard): the proxy modal stops pre-filling new scopes with an unrelated proxy** — adding a new scope assignment no longer inherits a previously-selected proxy's configuration. ([#4312](https://github.com/diegosouzapw/OmniRoute/pull/4312))
- **fix(open-sse): inner-ai stops silently rerouting unmatched models to `models[0]`** — an unmatched model id is no longer silently served by the first available model; the lookup now returns null and the request is handled explicitly. ([#4310](https://github.com/diegosouzapw/OmniRoute/pull/4310))
- **fix(pollinations): handle auth-required premium models (claude, gemini, midjourney)** — premium Pollinations models that require authentication are now handled correctly instead of failing. ([#4266](https://github.com/diegosouzapw/OmniRoute/pull/4266) — thanks @oyi77)
- **fix(codex): isolate the Spark quota scope** — Codex Spark usage is tracked under its own quota scope so it no longer bleeds into other Codex quotas. ([#4293](https://github.com/diegosouzapw/OmniRoute/pull/4293) — thanks @xz-dev)
- **fix(dashboard): improve the API "try it" functionality** — fixes the request path used by the dashboard's API "try it" panel. ([#4296](https://github.com/diegosouzapw/OmniRoute/pull/4296) — thanks @edrickrenan)
- **fix: polyfill `crypto.randomUUID` for non-secure contexts** — restores UUID generation when the dashboard is served over a non-secure (plain-HTTP) origin where `crypto.randomUUID` is unavailable. ([#4287](https://github.com/diegosouzapw/OmniRoute/pull/4287) — thanks @pizzav-xyz)
- **fix(proxy): allow concurrent proxy dispatcher streams** — the proxy dispatcher no longer serializes streams, so concurrent requests through a proxied connection run in parallel. ([#4288](https://github.com/diegosouzapw/OmniRoute/pull/4288) — thanks @wilsonicdev)
- **fix(build): co-locate llmlingua SLM optionals into `dist/node_modules` (postinstall)** — the optional llmlingua SLM packages are co-located into the standalone build so the compression worker can actually spawn in production. ([#4286](https://github.com/diegosouzapw/OmniRoute/pull/4286))
- **fix(mitm): surface AgentBridge traffic in the Traffic Inspector (D4 ingest)** — AgentBridge requests now appear in the Traffic Inspector. ([#4285](https://github.com/diegosouzapw/OmniRoute/pull/4285))
- **fix(sse): surface undici `err.cause` on dispatcher failure** — dispatcher failures now flatten the cause chain (and `AggregateError`s) into the error detail for diagnosability. ([#4281](https://github.com/diegosouzapw/OmniRoute/pull/4281))
- **fix(cli): harden `launch`/`launch-codex` with free-claude-code patterns** — the launchers adopt the hardened launch patterns ported from free-claude-code. ([#4278](https://github.com/diegosouzapw/OmniRoute/pull/4278))
- **fix(compression): end-to-end audit — fixes across the whole compression flow** — a sweep of the compression pipeline fixing ultra/aggressive/lossless edge cases, accessibility-anchor handling, language detection, and mode decoupling. ([#4323](https://github.com/diegosouzapw/OmniRoute/pull/4323))
### 🧪 Tests
- **test: align two tests left red by merged PRs** — re-aligns the db-rules classification count (#4335) and the LMArena split-cookie metadata test (#4271) after concurrent merges. ([#4346](https://github.com/diegosouzapw/OmniRoute/pull/4346))
- **test(ci): reconcile the release/v3.8.30 baseline + test drift** — reconciles quality baselines and drifted tests accumulated on the release branch. ([#4276](https://github.com/diegosouzapw/OmniRoute/pull/4276))
### 📝 Maintenance
- **refactor(combo): `ComboContext` + extract `phaseComboSetup` (god-file split, phase 1)** — begins decomposing the combo god-file by extracting combo setup into a context object, without touching dispatch/semaphore logic. ([#4326](https://github.com/diegosouzapw/OmniRoute/pull/4326))
- **feat(quality): cap test-file size — anti-reinflation Layer 1** — freezes the existing god-tests and caps new test files at 800 lines to stop re-inflation. ([#4273](https://github.com/diegosouzapw/OmniRoute/pull/4273))
- **feat(quality): seed per-module mutationScore floors + a blocking aggregation ratchet (T3)** — adds per-module mutation-score floors with a blocking aggregate gate. ([#4305](https://github.com/diegosouzapw/OmniRoute/pull/4305))
- **feat(quality): make the a11y gate real (`@axe-core/playwright` in nightly)** — wires the previously-phantom accessibility gate into the nightly run with real baselines. ([#4321](https://github.com/diegosouzapw/OmniRoute/pull/4321))
- **feat(quality): unblock R1 — test-redundancy measurement via `disableBail`** — enables the test-redundancy measurement that was previously blocked by fail-fast. ([#4322](https://github.com/diegosouzapw/OmniRoute/pull/4322))
- **fix(quality): the complexity gate now covers `bin/` + `electron/`, and tracked-artifacts runs in pre-commit** — extends the complexity gate's scope and moves the tracked-artifacts check into the pre-commit hook. ([#4318](https://github.com/diegosouzapw/OmniRoute/pull/4318))
- **fix(quality): restore release/v3.8.30 green — 3 latent reds from concurrent merges** — fixes three latent test reds surfaced by concurrent merges into the release branch. ([#4335](https://github.com/diegosouzapw/OmniRoute/pull/4335))
- **fix(combo): keep `phaseComboSetup` under the complexity ceiling** — extracts a helper so the new combo setup phase stays under the complexity gate. ([#4338](https://github.com/diegosouzapw/OmniRoute/pull/4338))
- **ci(mutation): split over-budget batches by range/pair so every batch fits the job cap** — re-splits the mutation batches so each fits the CI job budget. ([#4272](https://github.com/diegosouzapw/OmniRoute/pull/4272))
- **chore(ci): align the electron audit gate to the root advisory policy** — the electron-workspace audit gate now follows the same advisory policy as the root. ([#4275](https://github.com/diegosouzapw/OmniRoute/pull/4275))
- **chore(quality): reconcile the complexity/quality baselines across concurrent-merge drift** — rolls up the cycle's baseline reconciliations driven by concurrent merges into the release branch. ([#4330](https://github.com/diegosouzapw/OmniRoute/pull/4330), [#4336](https://github.com/diegosouzapw/OmniRoute/pull/4336), [#4370](https://github.com/diegosouzapw/OmniRoute/pull/4370))
- **docs: ban AI-generation footers in commits/PRs/CHANGELOG (Hard Rule #16)** — codifies the prohibition on AI-generation footers and bot co-author trailers. ([#4328](https://github.com/diegosouzapw/OmniRoute/pull/4328))
- **docs(design): add the OmniRoute design system and visual identity specification** — adds the design-system / visual-identity specification document. (thanks @diegosouzapw)
### 🔒 Security
- **fix(sse): harden the DuckDuckGo lite scraper sanitization (CodeQL)** — closes four HIGH CodeQL alerts in the no-key web-search scraper: `decodeEntities` now resolves `&amp;` **last** so an already-escaped entity (e.g. `&amp;lt;`) survives as literal text instead of being double-unescaped (`js/double-escaping`); `stripTags` decodes entities first, then strips tags in a loop to a fixpoint and drops any trailing unclosed `<…`, so entity-encoded markup like `&lt;script&gt;` can never reach the LLM/client as a live tag (`js/incomplete-multi-character-sanitization`); and the host checks in the search tests use `new URL().hostname` equality instead of substring `.includes` (`js/incomplete-url-substring-sanitization`). ([#4356](https://github.com/diegosouzapw/OmniRoute/pull/4356))
### 🔧 Dependencies
- **fix(deps): bump undici to 7.28.0 and dompurify to 3.4.11 (security)** — addresses the undici SOCKS5-TLS / cache advisories and the dompurify advisory. ([#4306](https://github.com/diegosouzapw/OmniRoute/pull/4306))
- **chore(deps): bump actions/checkout from 4 to 7** — CI checkout-action update. ([#4297](https://github.com/diegosouzapw/OmniRoute/pull/4297))
---

View File

@@ -36,7 +36,7 @@ endpoint with environment variables (it has no `--base-url` flag):
|----------|---------|
| `ANTHROPIC_BASE_URL` | Gateway root URL (Claude Code appends `/v1/messages`). **No `/v1` suffix.** |
| `ANTHROPIC_AUTH_TOKEN` | Sent as `Authorization: Bearer …` — use your OmniRoute access token / API key |
| `ANTHROPIC_API_KEY` | Alternative: sent as `x-api-key`. If both set, `AUTH_TOKEN` wins |
| `ANTHROPIC_API_KEY` | Alternative: sent as `x-api-key`. If both set, `ANTHROPIC_AUTH_TOKEN` wins |
| `ANTHROPIC_MODEL` | Force a specific model (overrides the `/model` picker default) |
| `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY` | `1` → the native `/model` picker lists `claude*`/`anthropic*` models from `/v1/models` |
| `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Cap output tokens per response (e.g. `65536`) |

View File

@@ -27,8 +27,7 @@ const DDG_USER_AGENT =
// The inner-content captures are HARD-BOUNDED ({0,N}?) and the whole body is truncated
// (MAX_HTML_BYTES) so adversarial/unclosed HTML can't cause catastrophic backtracking
// (ReDoS) — real titles/snippets are short. See CLAUDE.md PII learnings §1.
const ANCHOR_RE =
/<a\b([^>]*?class=['"][^'"]*result-link[^'"]*['"][^>]*)>([\s\S]{0,512}?)<\/a>/gi;
const ANCHOR_RE = /<a\b([^>]*?class=['"][^'"]*result-link[^'"]*['"][^>]*)>([\s\S]{0,512}?)<\/a>/gi;
const HREF_RE = /href=['"]([^'"]+)['"]/i;
const SNIPPET_RE =
/<td\b[^>]*?class=['"][^'"]*result-snippet[^'"]*['"][^>]*>([\s\S]{0,2048}?)<\/td>/gi;
@@ -36,17 +35,37 @@ const SNIPPET_RE =
const MAX_HTML_BYTES = 256 * 1024;
function decodeEntities(text: string): string {
return text
.replace(/&nbsp;/gi, " ")
.replace(/&amp;/gi, "&")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"')
.replace(/&#0*39;|&#x0*27;|&apos;/gi, "'");
return (
text
.replace(/&nbsp;/gi, " ")
.replace(/&lt;/gi, "<")
.replace(/&gt;/gi, ">")
.replace(/&quot;/gi, '"')
.replace(/&#0*39;|&#x0*27;|&apos;/gi, "'")
// Decode &amp; LAST so an already-escaped entity like "&amp;lt;" survives as the
// literal text "&lt;" instead of being double-unescaped into "<" — CodeQL
// js/double-escaping. When unescaping, &amp; must resolve after every other entity
// it could otherwise re-create.
.replace(/&amp;/gi, "&")
);
}
function stripTags(html: string): string {
return decodeEntities(html.replace(/<[^>]+>/g, "")).replace(/\s+/g, " ").trim();
// Decode entities FIRST so entity-encoded markup (e.g. "&lt;script&gt;") becomes real
// markup, then remove every tag. Looping to a fixpoint plus dropping a leftover
// unclosed "<…" guarantees the result can't carry "<script" (or any tag) through to the
// caller (LLM/client) — CodeQL js/incomplete-multi-character-sanitization. Bounded:
// each pass strictly shrinks the string, and MAX_HTML_BYTES caps the input upstream.
let text = decodeEntities(html);
let previous: string;
do {
previous = text;
text = text.replace(/<[^>]*>/g, "");
} while (text !== previous);
// After every well-formed tag is gone, any remaining "<" has no closing ">" — strip the
// trailing partial tag start so "<script…" can't survive.
text = text.replace(/<[^>]*$/, "");
return text.replace(/\s+/g, " ").trim();
}
/**

View File

@@ -106,6 +106,8 @@ const ENV_VAR_ALLOWLIST = new Set([
"NINEROUTER_API_KEY", // injected into the 9router subprocess at spawn (EMBEDDED-SERVICES.md)
"CLAUDE_CODE_MAX_OUTPUT_TOKENS", // Claude Code CLI's own env var (CODEX-CLI-CONFIGURATION.md)
"CODEX_HOME", // Codex CLI's own config-home env var (CODEX-CLI-CONFIGURATION.md)
"GEMINI_API_KEY", // Gemini CLI's own API-key env var, set by `omniroute setup-gemini` (REMOTE-MODE.md)
"GOOGLE_GEMINI_BASE_URL", // Gemini CLI's own base-URL env var, set by `omniroute setup-gemini` (REMOTE-MODE.md)
"REDIS_PORT", // docker-compose host-port override (DOCKER_GUIDE.md)
"AUTO_UPDATE_HOST_REPO_DIR", // docker-compose self-update mount (DOCKER_GUIDE.md)
"LINUX_GPG_KEY", // electron AppImage signing key, CI/build only (ELECTRON_GUIDE.md)

View File

@@ -95,3 +95,36 @@ test("parseDuckDuckGoLite tolerates a missing snippet (empty string, not crash)"
assert.equal(results.length, 1);
assert.equal(results[0].snippet, "");
});
test("parseDuckDuckGoLite does not double-unescape entities (CodeQL js/double-escaping)", () => {
// A snippet whose author literally wrote "5 &lt; 10" reaches us HTML-escaped as
// "5 &amp;lt; 10". Correct single-level decoding must yield the literal "5 &lt; 10",
// NOT collapse it into a real "<" ("5 < 10") by unescaping &amp; before &lt;.
const html = `<a href="https://e.com" class='result-link'>T</a>
<td class='result-snippet'>5 &amp;lt; 10</td>`;
const [r] = parseDuckDuckGoLite(html);
assert.equal(r.snippet, "5 &lt; 10");
assert.doesNotMatch(r.snippet, /5 < 10/, "must not double-unescape &amp;lt; into a real '<'");
});
test("parseDuckDuckGoLite never emits a live <script> from entity-encoded markup (CodeQL js/incomplete-multi-character-sanitization)", () => {
// Entity-encoded markup must not be decoded into a live "<script>" tag AFTER tag
// stripping — the result is surfaced to the LLM/client as a search snippet.
const html = `<a href="https://e.com" class='result-link'>safe &lt;script&gt;x&lt;/script&gt; title</a>
<td class='result-snippet'>ok &lt;script&gt;alert(1)&lt;/script&gt; done</td>`;
const [r] = parseDuckDuckGoLite(html);
assert.doesNotMatch(r.title, /<script/i, "title must not carry a live <script tag");
assert.doesNotMatch(r.snippet, /<script/i, "snippet must not carry a live <script tag");
assert.ok(r.title.includes("safe"), "benign title text is preserved");
assert.ok(r.snippet.includes("alert(1)"), "benign snippet text is preserved");
});
test("parseDuckDuckGoLite drops an unclosed entity-encoded tag start (no '<script' leak)", () => {
// "&lt;script" with no closing ">" decodes to "<script"; that partial tag must not
// survive into the output.
const html = `<a href="https://e.com" class='result-link'>safe title</a>
<td class='result-snippet'>tail &lt;script foo bar</td>`;
const [r] = parseDuckDuckGoLite(html);
assert.doesNotMatch(r.snippet, /<script/i);
assert.ok(r.snippet.startsWith("tail"));
});

View File

@@ -35,7 +35,14 @@ test("handleSearch fulfills duckduckgo-free via the HTML scraping path (no API k
});
assert.equal(result.success, true);
assert.ok(capturedUrl.includes("lite.duckduckgo.com"), "must hit the DDG lite endpoint");
// Compare the parsed hostname exactly (not a substring of the raw URL) so a host like
// "lite.duckduckgo.com.evil.test" could never satisfy the assertion — CodeQL
// js/incomplete-url-substring-sanitization.
assert.equal(
new URL(capturedUrl).hostname,
"lite.duckduckgo.com",
"must hit the DDG lite endpoint"
);
assert.equal(result.data.provider, "duckduckgo-free");
assert.equal(result.data.usage.search_cost_usd, 0, "free provider has zero cost");
assert.equal(result.data.results.length, 2);
@@ -51,7 +58,9 @@ test("handleSearch fulfills duckduckgo-free via the HTML scraping path (no API k
test("handleSearch fails over to duckduckgo-free when the primary provider errors", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async (url: string | URL) => {
if (String(url).includes("lite.duckduckgo.com")) {
// Route by exact parsed hostname rather than a raw-URL substring match — CodeQL
// js/incomplete-url-substring-sanitization.
if (new URL(String(url)).hostname === "lite.duckduckgo.com") {
return new Response(DDG_HTML, { status: 200, headers: { "content-type": "text/html" } });
}
// Primary (e.g. searxng on localhost) is unreachable.