@240xu/dsh-websearch 2.8.4 → 2.8.5

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.
@@ -30,20 +30,21 @@ export const ddgBackend = {
30
30
  throwIfSearchAborted(signal);
31
31
  const params = new URLSearchParams({ q: request.query, b: Math.random().toString().slice(2) });
32
32
  applyDdgFilters(params, normalizeFilters(request.filters));
33
- const body = params.toString();
33
+ const target = `${DDG_ENDPOINT}?${params.toString()}`;
34
34
  hooks.recordRequest?.({ endpoint: DDG_ENDPOINT, toolName: "html", body: { q: request.query }, headers: {} });
35
35
 
36
36
  let text;
37
37
  try {
38
- const res = await egressFetch(DDG_ENDPOINT, {
39
- method: "POST",
38
+ // GET form: DDG's html endpoint still serves the same results page, and
39
+ // unlike the POST form it is not rejected by the endpoint's bot wall
40
+ // for non-browser clients (POST is JA3 + method double-gated).
41
+ const res = await egressFetch(target, {
42
+ method: "GET",
40
43
  redirect: "error",
41
44
  headers: {
42
- "Content-Type": "application/x-www-form-urlencoded",
43
45
  "Accept": "text/html",
44
46
  "User-Agent": "dsh-websearch/2.3",
45
47
  },
46
- body,
47
48
  ...(signal !== undefined ? { signal } : {}),
48
49
  });
49
50
  if (!res.ok) {
@@ -10,6 +10,14 @@
10
10
  //
11
11
  // When no matching proxy env is set, or the target host sits in NO_PROXY, it
12
12
  // defers to the native fetch, so behavior everywhere else is unchanged.
13
+ //
14
+ // Optional TLS-egress forwarder: when DSH_EGRESS_FORWARDER is set (a local
15
+ // sidecar exposing a POST /fwd API backed by a Chromium TLS stack), requests
16
+ // are relayed through it with the upstream proxy carried in the sidecar spec
17
+ // (DSH_EGRESS_FORWARDER_PROXY, defaulting to HTTPS_PROXY). This exists because
18
+ // some upstreams (Exa, DuckDuckGo) gate on the client TLS fingerprint (JA3)
19
+ // rather than on the exit IP; the Node TLS fingerprint is rejected where a
20
+ // real browser's is accepted. Unset the env and behavior is byte-identical.
13
21
 
14
22
  import http from "node:http";
15
23
  import https from "node:https";
@@ -17,13 +25,33 @@ import https from "node:https";
17
25
  const REDIRECT_STATUS = new Set([301, 302, 303, 307, 308]);
18
26
  const MAX_REDIRECTS = 5;
19
27
 
20
- /** fetch-compatible entry: tunnels via HTTPS_PROXY/HTTP_PROXY when configured. */
28
+ /** fetch-compatible entry: forwarder first, then HTTPS_PROXY/HTTP_PROXY tunnel. */
21
29
  export function egressFetch(url, init = {}) {
30
+ const forwarder = forwarderFor(url);
31
+ if (forwarder !== undefined) return forwarderFetch(String(url), init, forwarder);
22
32
  const proxy = proxyFor(url);
23
33
  if (proxy === undefined) return fetch(url, init);
24
34
  return tunneledFetch(String(url), init, proxy, 0);
25
35
  }
26
36
 
37
+ /** Resolve the TLS-egress forwarder for a request target (NO_PROXY aware). */
38
+ function forwarderFor(url) {
39
+ let target;
40
+ try {
41
+ target = new URL(String(url));
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ if (target.protocol !== "https:" && target.protocol !== "http:") return undefined;
46
+ const env = process.env;
47
+ const endpoint = env.DSH_EGRESS_FORWARDER;
48
+ if (endpoint === undefined || endpoint === "") return undefined;
49
+ if (inNoProxy(target, env.NO_PROXY || env.no_proxy)) return undefined;
50
+ const proxy = env.DSH_EGRESS_FORWARDER_PROXY
51
+ || env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || "";
52
+ return { endpoint, proxy: proxy === "" ? undefined : proxy };
53
+ }
54
+
27
55
  /** Resolve the proxy URL for a request target (NO_PROXY aware). */
28
56
  function proxyFor(url) {
29
57
  let target;
@@ -110,6 +138,43 @@ async function tunneledFetch(url, init, proxy, depth) {
110
138
  return response;
111
139
  }
112
140
 
141
+ /**
142
+ * Relay through the TLS-egress sidecar: POST /fwd with an
143
+ * x-sidecar-spec base64 JSON header; the sidecar performs the upstream
144
+ * request with its own (Chromium) TLS stack and streams the upstream
145
+ * status/headers/body back verbatim. Upstream dial failures surface as
146
+ * 502 + x-sidecar-error, which we rethrow as fetch-style TypeError.
147
+ */
148
+ async function forwarderFetch(url, init, forwarder) {
149
+ const method = String(init.method ?? "GET").toUpperCase();
150
+ const headers = normalizeHeaders(init.headers);
151
+ const body = normalizeBody(init.body);
152
+ const spec = { url, method, headers, timeout_ms: 90000 };
153
+ if (forwarder.proxy !== undefined) spec.proxy = forwarder.proxy;
154
+ if (init.redirect !== undefined) spec.redirect = init.redirect;
155
+ let response;
156
+ try {
157
+ response = await fetch(forwarder.endpoint, {
158
+ method: "POST",
159
+ headers: {
160
+ "content-type": "application/octet-stream",
161
+ "x-sidecar-spec": Buffer.from(JSON.stringify(spec)).toString("base64"),
162
+ },
163
+ ...(body !== undefined ? { body } : {}),
164
+ ...(init.signal !== undefined ? { signal: init.signal } : {}),
165
+ });
166
+ } catch (error) {
167
+ throw new TypeError(`fetch failed: forwarder unreachable (${sanitize(error)})`, { cause: error });
168
+ }
169
+ const marker = response.headers.get("x-sidecar-error");
170
+ if (response.status === 502 && marker !== null) {
171
+ let detail = "";
172
+ try { detail = (await response.text()).slice(0, 200); } catch {}
173
+ throw new TypeError(`fetch failed: forwarder ${marker}${detail !== "" ? ` (${detail})` : ""}`);
174
+ }
175
+ return response;
176
+ }
177
+
113
178
  /* ------------------------------------------------------------------ */
114
179
  /* transports */
115
180
  /* ------------------------------------------------------------------ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-websearch",
3
- "version": "2.8.4",
3
+ "version": "2.8.5",
4
4
  "description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends — Exa + Parallel + DDG + SearXNG (keyless), DeepSeek + Anthropic + OpenAI + Brave + Tavily + Serper + Mojeek (key-gated via Settings credentials). Registers provider id \"unified\" for zero-config web_search. 统一网页搜索聚合:11 后端并发扇出、去重合并,零依赖。See CHANGELOG.md for version history.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",