@demigodmode/pi-web-agent 1.11.0 → 1.13.0

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +2 -0
  3. package/dist/backends/config.d.ts +13 -0
  4. package/dist/backends/config.js +44 -1
  5. package/dist/backends/doctor.js +27 -29
  6. package/dist/backends/factory.d.ts +18 -7
  7. package/dist/backends/factory.js +126 -99
  8. package/dist/backends/failure.d.ts +11 -0
  9. package/dist/backends/failure.js +34 -0
  10. package/dist/backends/fallback-policy.d.ts +31 -0
  11. package/dist/backends/fallback-policy.js +239 -0
  12. package/dist/backends/provider-failure.d.ts +21 -0
  13. package/dist/backends/provider-failure.js +110 -0
  14. package/dist/backends/provider-health.d.ts +29 -0
  15. package/dist/backends/provider-health.js +49 -0
  16. package/dist/commands/web-agent-config.d.ts +14 -1
  17. package/dist/commands/web-agent-config.js +75 -3
  18. package/dist/extension.js +47 -3
  19. package/dist/extract/bot-check.d.ts +1 -0
  20. package/dist/extract/bot-check.js +10 -0
  21. package/dist/extract/readability.d.ts +4 -0
  22. package/dist/extract/readability.js +67 -3
  23. package/dist/extract/section-selector.d.ts +14 -0
  24. package/dist/extract/section-selector.js +233 -0
  25. package/dist/fetch/destination-policy.d.ts +32 -0
  26. package/dist/fetch/destination-policy.js +24 -0
  27. package/dist/fetch/firecrawl-fetch.d.ts +1 -1
  28. package/dist/fetch/firecrawl-fetch.js +74 -46
  29. package/dist/fetch/guard-proxy-fetch.d.ts +17 -0
  30. package/dist/fetch/guard-proxy-fetch.js +82 -0
  31. package/dist/fetch/guard-proxy.d.ts +58 -0
  32. package/dist/fetch/guard-proxy.js +420 -0
  33. package/dist/fetch/guarded-fetch.d.ts +7 -0
  34. package/dist/fetch/guarded-fetch.js +75 -0
  35. package/dist/fetch/headless-fetch.d.ts +11 -2
  36. package/dist/fetch/headless-fetch.js +190 -13
  37. package/dist/fetch/http-fetch.d.ts +1 -1
  38. package/dist/fetch/http-fetch.js +29 -8
  39. package/dist/fetch/network-guard.d.ts +82 -0
  40. package/dist/fetch/network-guard.js +275 -0
  41. package/dist/orchestration/answer-synthesizer.js +2 -0
  42. package/dist/orchestration/evidence-quality.d.ts +3 -2
  43. package/dist/orchestration/evidence-quality.js +2 -1
  44. package/dist/orchestration/index.d.ts +26 -7
  45. package/dist/orchestration/index.js +9 -2
  46. package/dist/orchestration/research-orchestrator.d.ts +23 -7
  47. package/dist/orchestration/research-orchestrator.js +60 -26
  48. package/dist/orchestration/research-types.d.ts +13 -1
  49. package/dist/orchestration/research-worker.d.ts +2 -4
  50. package/dist/orchestration/research-worker.js +51 -15
  51. package/dist/orchestration/stop-decider.js +3 -1
  52. package/dist/presentation/config-store.js +6 -0
  53. package/dist/presentation/explore-presentation.js +3 -1
  54. package/dist/presentation/fetch-presentation.js +16 -9
  55. package/dist/presentation/search-presentation.d.ts +2 -1
  56. package/dist/presentation/search-presentation.js +13 -1
  57. package/dist/readers/resolver.d.ts +3 -7
  58. package/dist/search/brave.d.ts +1 -2
  59. package/dist/search/brave.js +23 -80
  60. package/dist/search/duckduckgo.d.ts +7 -3
  61. package/dist/search/duckduckgo.js +17 -18
  62. package/dist/search/exa.d.ts +1 -2
  63. package/dist/search/exa.js +15 -76
  64. package/dist/search/fanout.d.ts +12 -0
  65. package/dist/search/fanout.js +86 -47
  66. package/dist/search/json-provider.d.ts +32 -0
  67. package/dist/search/json-provider.js +76 -0
  68. package/dist/search/searxng.d.ts +1 -2
  69. package/dist/search/searxng.js +15 -57
  70. package/dist/search/tavily.d.ts +1 -2
  71. package/dist/search/tavily.js +17 -74
  72. package/dist/search/youcom.d.ts +4 -2
  73. package/dist/search/youcom.js +49 -75
  74. package/dist/tools/web-explore.d.ts +9 -0
  75. package/dist/tools/web-explore.js +16 -2
  76. package/dist/tools/web-fetch-headless.d.ts +3 -5
  77. package/dist/tools/web-fetch-headless.js +3 -3
  78. package/dist/tools/web-fetch.d.ts +3 -5
  79. package/dist/tools/web-fetch.js +3 -3
  80. package/dist/tools/web-search.js +41 -103
  81. package/dist/types.d.ts +48 -0
  82. package/package.json +3 -3
@@ -1,13 +1,53 @@
1
1
  import { chromium } from 'playwright';
2
- import { extractReadableContentSafely } from '../extract/readability.js';
2
+ import { extractReadableContentForQuery, extractReadableContentSafely } from '../extract/readability.js';
3
+ import { hasBotCheckContent } from '../extract/bot-check.js';
3
4
  import { resolveBrowserExecutable } from './browser-resolution.js';
5
+ import { BLOCKED_HEADER } from './guard-proxy.js';
6
+ import { BLOCKED_PRIVATE_ADDRESS, BlockedAddressError, UPSTREAM_PROXY_REFUSED } from './network-guard.js';
4
7
  function cleanupRenderedText(text) {
5
8
  let cleaned = text.replace(/(Show more)(\s+\1){1,}/gi, '$1');
6
9
  cleaned = cleaned.replace(/(Privacy Terms)(\s+\1){1,}/gi, '$1');
7
10
  cleaned = cleaned.replace(/\s+/g, ' ').trim();
8
11
  return cleaned;
9
12
  }
10
- export async function headlessFetch(url, { configuredPath, proxy, resolveBrowser = (options) => resolveBrowserExecutable({ configuredPath: options?.configuredPath }), launchBrowser = ({ executablePath, headless, proxy }) => chromium.launch(executablePath ? { executablePath, headless, ...(proxy ? { proxy } : {}) } : { headless, ...(proxy ? { proxy } : {}) }), now = () => Date.now() } = {}) {
13
+ function errorResult(url, code, message) {
14
+ const guard = code === BLOCKED_PRIVATE_ADDRESS || code === UPSTREAM_PROXY_REFUSED;
15
+ return {
16
+ status: 'error',
17
+ url,
18
+ metadata: { method: 'headless', cacheHit: false },
19
+ error: { code, message, ...(guard ? { failure: { kind: 'guard_refused' } } : {}) }
20
+ };
21
+ }
22
+ function hostnameOf(url) {
23
+ try {
24
+ return new URL(url).hostname;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ /** Same normalization the guard applies, so refusal hosts and navigation hosts compare equal. */
31
+ function normalizeHost(host) {
32
+ return host.replace(/^\[|\]$/g, '').toLowerCase().replace(/\.+$/, '');
33
+ }
34
+ export async function headlessFetch(url, { configuredPath, query, proxy, guard, guardProxy, resolveBrowser = (options) => resolveBrowserExecutable({ configuredPath: options?.configuredPath }), launchBrowser = ({ executablePath, headless, proxy }) => chromium.launch(executablePath ? { executablePath, headless, ...(proxy ? { proxy } : {}) } : { headless, ...(proxy ? { proxy } : {}) }), now = () => Date.now() } = {}) {
35
+ if (guard) {
36
+ const hostname = hostnameOf(url);
37
+ if (!guardProxy) {
38
+ // Enforcement lives in the guard proxy. Without it, loading the page would be unguarded.
39
+ return errorResult(url, BLOCKED_PRIVATE_ADDRESS, `Blocked ${hostname ?? url}: the browser could not enforce the private address guard.`);
40
+ }
41
+ if (hostname) {
42
+ // Early, clearer refusal for an obviously blocked main url. The guard
43
+ // proxy is what actually enforces the policy for every connection.
44
+ const verdict = await guard.checkHost(hostname);
45
+ if (!verdict.allowed) {
46
+ const error = new BlockedAddressError(verdict.host, verdict.address);
47
+ return errorResult(url, error.code, error.message);
48
+ }
49
+ }
50
+ }
11
51
  const resolved = await resolveBrowser({ configuredPath });
12
52
  if (!resolved.ok && resolved.error.code === 'CONFIGURED_BROWSER_NOT_FOUND') {
13
53
  return {
@@ -17,29 +57,159 @@ export async function headlessFetch(url, { configuredPath, proxy, resolveBrowser
17
57
  error: resolved.error
18
58
  };
19
59
  }
60
+ let enforcement;
61
+ if (guard && guardProxy) {
62
+ let activeProxy;
63
+ try {
64
+ activeProxy = await guardProxy();
65
+ }
66
+ catch {
67
+ // The guard proxy could not start (backend set closed, or the listener
68
+ // failed). Enforcement lives there, so the browser must not launch.
69
+ return errorResult(url, BLOCKED_PRIVATE_ADDRESS, `Blocked ${hostnameOf(url) ?? url}: the private address guard is not available, so the browser was not started.`);
70
+ }
71
+ const client = activeProxy.client('headless');
72
+ enforcement = {
73
+ proxy: activeProxy,
74
+ username: client.username,
75
+ since: activeProxy.sequence(),
76
+ // `<-loopback>` removes Chromium's implicit loopback bypass, so localhost
77
+ // and link-local connections go through the guard proxy too.
78
+ launchProxy: { server: client.server, username: client.username, password: client.password, bypass: '<-loopback>' }
79
+ };
80
+ }
81
+ const effectiveProxy = enforcement ? enforcement.launchProxy : proxy;
20
82
  const browserName = resolved.ok ? resolved.browser : 'chromium';
21
83
  const launchOptions = resolved.ok
22
- ? { executablePath: resolved.executablePath, headless: true, ...(proxy ? { proxy } : {}) }
23
- : { headless: true, ...(proxy ? { proxy } : {}) };
84
+ ? { executablePath: resolved.executablePath, headless: true, ...(effectiveProxy ? { proxy: effectiveProxy } : {}) }
85
+ : { headless: true, ...(effectiveProxy ? { proxy: effectiveProxy } : {}) };
86
+ // Browser requests that failed or were refused by the proxy, from passive
87
+ // page events only. Navigation hosts decide whether a refusal caused the
88
+ // navigation error; subresource failures are counted one per request.
89
+ const failedNavigationHosts = new Set();
90
+ const failedSubresourceHosts = [];
91
+ const seenRequests = new WeakSet();
92
+ const refusals = () => (enforcement ? enforcement.proxy.refusalsSince(enforcement.username, enforcement.since) : []);
93
+ const navigationRefusal = () => refusals().find((entry) => failedNavigationHosts.has(entry.host));
94
+ const subresourceRefusals = () => {
95
+ const refusedHosts = new Set(refusals().map((entry) => entry.host));
96
+ return failedSubresourceHosts.filter((host) => refusedHosts.has(host)).length;
97
+ };
24
98
  let browser;
25
99
  let context;
26
100
  let page;
27
101
  try {
28
102
  browser = await launchBrowser(launchOptions);
29
- context = await browser.newContext();
30
- page = await context.newPage();
103
+ // Service workers can fetch on a page's behalf; blocking them keeps the page's traffic simple to account for.
104
+ context = await browser.newContext(enforcement ? { serviceWorkers: 'block' } : undefined);
105
+ if (enforcement) {
106
+ // Watch every page in the context: popups opened by the page can hit blocked
107
+ // hosts too. Only the primary page's main-frame navigation is "the navigation".
108
+ let primaryPage;
109
+ const recordRequest = (request) => {
110
+ try {
111
+ if (!request || seenRequests.has(request))
112
+ return;
113
+ seenRequests.add(request);
114
+ const host = normalizeHost(new URL(request.url()).hostname);
115
+ if (primaryPage && request.isNavigationRequest() && request.frame() === primaryPage.mainFrame()) {
116
+ failedNavigationHosts.add(host);
117
+ }
118
+ else {
119
+ failedSubresourceHosts.push(host);
120
+ }
121
+ }
122
+ catch {
123
+ // Unparseable URL or a detached frame: nothing to attribute.
124
+ }
125
+ };
126
+ const watchPage = (watched) => {
127
+ watched?.on?.('requestfailed', recordRequest);
128
+ watched?.on?.('response', (response) => {
129
+ try {
130
+ if (response.headers()[BLOCKED_HEADER])
131
+ recordRequest(response.request());
132
+ }
133
+ catch {
134
+ // ignore
135
+ }
136
+ });
137
+ // Best effort: Playwright doesn't say why a WebSocket died, so an error or a
138
+ // close before any frame counts as failed. Only refused hosts get counted.
139
+ watched?.on?.('websocket', (ws) => {
140
+ let host;
141
+ try {
142
+ host = normalizeHost(new URL(ws.url()).hostname);
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ let framed = false;
148
+ let recorded = false;
149
+ const fail = () => {
150
+ if (recorded)
151
+ return;
152
+ recorded = true;
153
+ failedSubresourceHosts.push(host);
154
+ };
155
+ ws.on?.('framereceived', () => (framed = true));
156
+ ws.on?.('framesent', () => (framed = true));
157
+ ws.on?.('socketerror', fail);
158
+ ws.on?.('close', () => {
159
+ if (!framed)
160
+ fail();
161
+ });
162
+ });
163
+ };
164
+ const watchedPages = new WeakSet();
165
+ const watchOnce = (candidate) => {
166
+ if (!candidate || watchedPages.has(candidate))
167
+ return;
168
+ watchedPages.add(candidate);
169
+ watchPage(candidate);
170
+ };
171
+ // Registered before newPage(), so the primary page's own 'page' event is covered too.
172
+ context.on?.('page', watchOnce);
173
+ page = await context.newPage();
174
+ primaryPage = page;
175
+ watchOnce(page);
176
+ }
177
+ else {
178
+ page = await context.newPage();
179
+ }
31
180
  const startedAt = now();
32
- await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
181
+ const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 });
182
+ if (enforcement && response?.headers?.()[BLOCKED_HEADER]) {
183
+ // Normally already recorded by the 'response' event; this covers it if not.
184
+ try {
185
+ const request = response.request?.();
186
+ if (request && !seenRequests.has(request)) {
187
+ seenRequests.add(request);
188
+ failedNavigationHosts.add(normalizeHost(new URL(request.url()).hostname));
189
+ }
190
+ }
191
+ catch {
192
+ // ignore
193
+ }
194
+ const cause = navigationRefusal();
195
+ if (cause)
196
+ return errorResult(url, cause.error.code, cause.error.message);
197
+ }
33
198
  await page.waitForLoadState('load', { timeout: 10000 });
34
199
  await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => undefined);
35
200
  const html = await page.content();
36
201
  const finishedAt = now();
37
- const extraction = extractReadableContentSafely(html);
202
+ const blockedSubresources = subresourceRefusals();
203
+ const baselineExtraction = extractReadableContentSafely(html);
204
+ const queryExtraction = query ? extractReadableContentForQuery(html, query) : undefined;
205
+ const extraction = queryExtraction ?? baselineExtraction;
206
+ const cleanedBaselineText = cleanupRenderedText(baselineExtraction.content.text);
38
207
  const cleanedContent = {
39
208
  ...extraction.content,
40
- text: cleanupRenderedText(extraction.content.text)
209
+ text: cleanupRenderedText(extraction.content.text),
210
+ ...(hasBotCheckContent(html, 'html') ? { botCheck: true } : {})
41
211
  };
42
- if (!cleanedContent.text || cleanedContent.text.length < 40) {
212
+ if (!cleanedBaselineText || cleanedBaselineText.length < 40) {
43
213
  return {
44
214
  status: 'blocked',
45
215
  url,
@@ -47,7 +217,8 @@ export async function headlessFetch(url, { configuredPath, proxy, resolveBrowser
47
217
  method: 'headless',
48
218
  cacheHit: false,
49
219
  browser: browserName,
50
- navigationMs: finishedAt - startedAt
220
+ navigationMs: finishedAt - startedAt,
221
+ ...(blockedSubresources > 0 ? { blockedSubresources } : {})
51
222
  },
52
223
  error: {
53
224
  code: 'HEADLESS_EXTRACTION_WEAK',
@@ -64,18 +235,24 @@ export async function headlessFetch(url, { configuredPath, proxy, resolveBrowser
64
235
  cacheHit: false,
65
236
  browser: browserName,
66
237
  navigationMs: finishedAt - startedAt,
67
- truncated: cleanedContent.text.length >= 4000
238
+ truncated: queryExtraction?.omitted ?? cleanedContent.text.length >= 4000,
239
+ ...(blockedSubresources > 0 ? { blockedSubresources } : {})
68
240
  }
69
241
  };
70
242
  }
71
243
  catch (error) {
244
+ const cause = navigationRefusal();
245
+ if (cause)
246
+ return errorResult(url, cause.error.code, cause.error.message);
247
+ const blockedSubresources = subresourceRefusals();
72
248
  return {
73
249
  status: 'error',
74
250
  url,
75
251
  metadata: {
76
252
  method: 'headless',
77
253
  cacheHit: false,
78
- browser: browserName
254
+ browser: browserName,
255
+ ...(blockedSubresources > 0 ? { blockedSubresources } : {})
79
256
  },
80
257
  error: {
81
258
  code: 'HEADLESS_NAVIGATION_FAILED',
@@ -1,4 +1,4 @@
1
1
  import type { WebFetchResponse } from '../types.js';
2
2
  export declare function createHttpFetcher({ fetchImpl }?: {
3
3
  fetchImpl?: typeof fetch;
4
- }): (url: string) => Promise<WebFetchResponse>;
4
+ }): (url: string, query?: string) => Promise<WebFetchResponse>;
@@ -1,4 +1,6 @@
1
- import { extractReadableContentSafely } from '../extract/readability.js';
1
+ import { extractReadableContentForQuery, extractReadableContentSafely } from '../extract/readability.js';
2
+ import { hasBotCheckContent } from '../extract/bot-check.js';
3
+ import { findGuardError } from './network-guard.js';
2
4
  function looksLikeScriptShell(html) {
3
5
  const lower = html.toLowerCase();
4
6
  return lower.includes('<script') && (lower.includes('id="app"') || lower.includes('id="root"'));
@@ -14,8 +16,22 @@ function isWeakHttpContent(options) {
14
16
  return veryShortBody && (lowDensity || hasGenericShellMarker);
15
17
  }
16
18
  export function createHttpFetcher({ fetchImpl = fetch } = {}) {
17
- return async function httpFetch(url) {
18
- const response = await fetchImpl(url);
19
+ return async function httpFetch(url, query) {
20
+ let response;
21
+ try {
22
+ response = await fetchImpl(url);
23
+ }
24
+ catch (error) {
25
+ const blocked = findGuardError(error);
26
+ if (!blocked)
27
+ throw error;
28
+ return {
29
+ status: 'error',
30
+ url,
31
+ metadata: { method: 'http', cacheHit: false },
32
+ error: { code: blocked.code, message: blocked.message, failure: { kind: 'guard_refused' } }
33
+ };
34
+ }
19
35
  const contentType = response.headers.get('content-type') ?? '';
20
36
  if (!contentType.includes('text/html')) {
21
37
  return {
@@ -25,11 +41,16 @@ export function createHttpFetcher({ fetchImpl = fetch } = {}) {
25
41
  };
26
42
  }
27
43
  const html = await response.text();
28
- const extraction = extractReadableContentSafely(html);
29
- const content = extraction.content;
44
+ const baselineExtraction = extractReadableContentSafely(html);
45
+ const queryExtraction = query ? extractReadableContentForQuery(html, query) : undefined;
46
+ const extraction = queryExtraction ?? baselineExtraction;
47
+ const content = {
48
+ ...extraction.content,
49
+ ...(hasBotCheckContent(html, 'html') ? { botCheck: true } : {})
50
+ };
30
51
  if (looksLikeScriptShell(html) ||
31
- content.text.length < 40 ||
32
- isWeakHttpContent({ html, title: content.title, text: content.text })) {
52
+ baselineExtraction.content.text.length < 40 ||
53
+ isWeakHttpContent({ html, title: baselineExtraction.content.title, text: baselineExtraction.content.text })) {
33
54
  return {
34
55
  status: 'needs_headless',
35
56
  url: response.url,
@@ -44,7 +65,7 @@ export function createHttpFetcher({ fetchImpl = fetch } = {}) {
44
65
  status: 'ok',
45
66
  url: response.url,
46
67
  content,
47
- metadata: { method: 'http', cacheHit: false, contentType, truncated: content.text.length >= 4000 }
68
+ metadata: { method: 'http', cacheHit: false, contentType, truncated: queryExtraction?.omitted ?? content.text.length >= 4000 }
48
69
  };
49
70
  };
50
71
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Decides whether a destination the *model* chose is safe to fetch (#53).
3
+ *
4
+ * web_explore fetches URLs the model picks, partly from pages it just read, so
5
+ * a hostile page can point it at cloud metadata (169.254.169.254), services on
6
+ * localhost, or the user's LAN. This module only answers "is this address
7
+ * allowed"; guarded-fetch.ts and headless-fetch.ts enforce it. User-configured
8
+ * endpoints (search APIs, SearXNG, Firecrawl, the proxy) never go through it.
9
+ */
10
+ export type Cidr = {
11
+ family: 4 | 6;
12
+ network: bigint;
13
+ prefix: number;
14
+ };
15
+ export type LookupFn = (host: string) => Promise<Array<{
16
+ address: string;
17
+ family: number;
18
+ }>>;
19
+ export type NetworkGuardConfig = {
20
+ allowRanges?: string[];
21
+ };
22
+ export type GuardVerdict = {
23
+ allowed: true;
24
+ unresolved?: true;
25
+ } | {
26
+ allowed: false;
27
+ host: string;
28
+ address: string;
29
+ };
30
+ export type HostResolution = {
31
+ status: 'allowed';
32
+ host: string;
33
+ addresses: string[];
34
+ } | {
35
+ status: 'blocked';
36
+ host: string;
37
+ address: string;
38
+ } | {
39
+ status: 'unresolved';
40
+ host: string;
41
+ };
42
+ export type NetworkGuard = {
43
+ isBlockedAddress(address: string): boolean;
44
+ /** Resolves once and checks every answer. The guard proxy connects to one of `addresses`, never re-resolving. */
45
+ resolveHost(host: string): Promise<HostResolution>;
46
+ checkHost(host: string): Promise<GuardVerdict>;
47
+ assertUrlAllowed(url: string): Promise<void>;
48
+ };
49
+ export declare const BLOCKED_PRIVATE_ADDRESS = "BLOCKED_PRIVATE_ADDRESS";
50
+ export declare class BlockedAddressError extends Error {
51
+ readonly host: string;
52
+ readonly address: string;
53
+ readonly code = "BLOCKED_PRIVATE_ADDRESS";
54
+ constructor(host: string, address: string);
55
+ }
56
+ /** No address could be verified, so we refuse rather than let something else resolve it. */
57
+ export declare class UnverifiedDestinationError extends Error {
58
+ readonly host: string;
59
+ readonly code = "BLOCKED_PRIVATE_ADDRESS";
60
+ constructor(host: string);
61
+ }
62
+ export declare const UPSTREAM_PROXY_REFUSED = "UPSTREAM_PROXY_REFUSED";
63
+ /** The user's upstream proxy would not accept the approved IP. We never retry by hostname. */
64
+ export declare class UpstreamProxyRefusedError extends Error {
65
+ readonly host: string;
66
+ readonly target: string;
67
+ readonly status: number | string;
68
+ readonly code = "UPSTREAM_PROXY_REFUSED";
69
+ constructor(host: string, target: string, status: number | string);
70
+ }
71
+ export type GuardError = BlockedAddressError | UnverifiedDestinationError | UpstreamProxyRefusedError;
72
+ export declare function findGuardError(error: unknown): GuardError | undefined;
73
+ /** undici wraps connect errors as `TypeError: fetch failed` with the real error in `cause`. */
74
+ export declare function findBlockedAddressError(error: unknown): BlockedAddressError | undefined;
75
+ export declare function parseCidr(text: string): Cidr | undefined;
76
+ /** Parses an allow list, dropping invalid entries and anything that allows every address. */
77
+ export declare function usableAllowRanges(allowRanges?: string[]): Cidr[];
78
+ export declare function createNetworkGuard(config?: NetworkGuardConfig, { lookup, lookupTimeoutMs }?: {
79
+ lookup?: LookupFn;
80
+ /** A lookup that hasn't answered by then counts as unresolved, so no caller waits on DNS forever. */
81
+ lookupTimeoutMs?: number;
82
+ }): NetworkGuard;