@pentoshi/clai 0.10.5 → 0.11.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 (77) hide show
  1. package/README.md +32 -0
  2. package/dist/agent/runner.js +41 -3
  3. package/dist/agent/runner.js.map +1 -1
  4. package/dist/commands/providers.js +28 -0
  5. package/dist/commands/providers.js.map +1 -1
  6. package/dist/commands/search-providers.d.ts +50 -0
  7. package/dist/commands/search-providers.js +134 -0
  8. package/dist/commands/search-providers.js.map +1 -0
  9. package/dist/commands/update.js +1 -1
  10. package/dist/index.js +8 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/llm/provider.js +9 -6
  13. package/dist/llm/provider.js.map +1 -1
  14. package/dist/prompts/index.d.ts +1 -1
  15. package/dist/prompts/index.js +6 -0
  16. package/dist/prompts/index.js.map +1 -1
  17. package/dist/safety/classifier.js +40 -0
  18. package/dist/safety/classifier.js.map +1 -1
  19. package/dist/store/config.d.ts +5 -0
  20. package/dist/store/config.js +7 -0
  21. package/dist/store/config.js.map +1 -1
  22. package/dist/store/keys.d.ts +65 -0
  23. package/dist/store/keys.js +164 -28
  24. package/dist/store/keys.js.map +1 -1
  25. package/dist/tools/http.d.ts +12 -1
  26. package/dist/tools/http.js +8 -43
  27. package/dist/tools/http.js.map +1 -1
  28. package/dist/tools/registry.js +52 -0
  29. package/dist/tools/registry.js.map +1 -1
  30. package/dist/tools/shell.d.ts +25 -0
  31. package/dist/tools/shell.js +155 -6
  32. package/dist/tools/shell.js.map +1 -1
  33. package/dist/tools/web/audit.d.ts +154 -0
  34. package/dist/tools/web/audit.js +147 -0
  35. package/dist/tools/web/audit.js.map +1 -0
  36. package/dist/tools/web/budget.d.ts +76 -0
  37. package/dist/tools/web/budget.js +187 -0
  38. package/dist/tools/web/budget.js.map +1 -0
  39. package/dist/tools/web/capture.d.ts +201 -0
  40. package/dist/tools/web/capture.js +380 -0
  41. package/dist/tools/web/capture.js.map +1 -0
  42. package/dist/tools/web/fetch-core.d.ts +66 -0
  43. package/dist/tools/web/fetch-core.js +1123 -0
  44. package/dist/tools/web/fetch-core.js.map +1 -0
  45. package/dist/tools/web/fetch.d.ts +42 -0
  46. package/dist/tools/web/fetch.js +115 -0
  47. package/dist/tools/web/fetch.js.map +1 -0
  48. package/dist/tools/web/providers/brave.d.ts +46 -0
  49. package/dist/tools/web/providers/brave.js +263 -0
  50. package/dist/tools/web/providers/brave.js.map +1 -0
  51. package/dist/tools/web/providers/duckduckgo.d.ts +47 -0
  52. package/dist/tools/web/providers/duckduckgo.js +248 -0
  53. package/dist/tools/web/providers/duckduckgo.js.map +1 -0
  54. package/dist/tools/web/providers/provider.d.ts +99 -0
  55. package/dist/tools/web/providers/provider.js +38 -0
  56. package/dist/tools/web/providers/provider.js.map +1 -0
  57. package/dist/tools/web/providers/tavily.d.ts +52 -0
  58. package/dist/tools/web/providers/tavily.js +285 -0
  59. package/dist/tools/web/providers/tavily.js.map +1 -0
  60. package/dist/tools/web/readable.d.ts +67 -0
  61. package/dist/tools/web/readable.js +248 -0
  62. package/dist/tools/web/readable.js.map +1 -0
  63. package/dist/tools/web/redact.d.ts +120 -0
  64. package/dist/tools/web/redact.js +155 -0
  65. package/dist/tools/web/redact.js.map +1 -0
  66. package/dist/tools/web/search.d.ts +51 -0
  67. package/dist/tools/web/search.js +389 -0
  68. package/dist/tools/web/search.js.map +1 -0
  69. package/dist/tools/web/ssrf-guard.d.ts +85 -0
  70. package/dist/tools/web/ssrf-guard.js +265 -0
  71. package/dist/tools/web/ssrf-guard.js.map +1 -0
  72. package/dist/tools/web/types.d.ts +331 -0
  73. package/dist/tools/web/types.js +71 -0
  74. package/dist/tools/web/types.js.map +1 -0
  75. package/dist/ui/spinner.js +87 -14
  76. package/dist/ui/spinner.js.map +1 -1
  77. package/package.json +3 -1
@@ -0,0 +1,389 @@
1
+ /**
2
+ * `web.search` registry handler.
3
+ *
4
+ * Resolves the active {@link SearchProviderId}, looks up the API key (if
5
+ * needed), dispatches a single outbound request via the registered
6
+ * adapter, validates each returned hit per Requirement 7.3, truncates to
7
+ * `maxResults`, and emits exactly one structured audit-log entry per
8
+ * invocation (Requirement 5.5).
9
+ *
10
+ * Error handling mirrors the design's error matrix: timeouts (1.8),
11
+ * missing keys (3.4), provider auth failures (6.1), rate limiting (6.2),
12
+ * network failures (6.3), parse failures (6.5), 5xx (6.6), and other
13
+ * non-2xx (1.9). Every failure surfaces as `ok=false` with a categorical
14
+ * `error.kind` and a human-readable message that names the active
15
+ * provider.
16
+ *
17
+ * Per Requirement 6.7 the handler issues exactly one outbound request
18
+ * attempt — there is no retry on transient failure.
19
+ *
20
+ * The provider modules register themselves into the shared
21
+ * {@link searchProviders} registry on import; we eagerly import them
22
+ * here so `web.search` can be invoked without any lazy-load surprises.
23
+ */
24
+ import { auditLog } from "../../store/logs.js";
25
+ import { getActiveSearchProvider } from "../../store/config.js";
26
+ import { getSearchProviderKey } from "../../store/keys.js";
27
+ import { buildSearchAuditPayload } from "./audit.js";
28
+ import { searchProviders, } from "./providers/provider.js";
29
+ // Importing the provider modules below ensures their side-effect
30
+ // registration into `searchProviders` runs before the handler is
31
+ // invoked. (DDG → keyless default; Brave / Tavily → optional.)
32
+ import "./providers/duckduckgo.js";
33
+ import "./providers/brave.js";
34
+ import "./providers/tavily.js";
35
+ import { DEFAULT_MAX_RESULTS, MAX_MAX_RESULTS, MAX_QUERY_LENGTH, MAX_SNIPPET_LENGTH, MAX_TITLE_LENGTH, MIN_MAX_RESULTS, MIN_QUERY_LENGTH, SEARCH_TIMEOUT_MS, } from "./types.js";
36
+ /**
37
+ * Run `web.search`. Always emits a single audit-log entry. Never
38
+ * throws — every failure mode surfaces as `ok=false`.
39
+ */
40
+ export async function webSearch(args, options = {}) {
41
+ // Validate args before resolving the provider so a malformed call
42
+ // never appears in the audit log under a real provider's id.
43
+ const validated = validateArgs(args);
44
+ if (!validated.ok) {
45
+ const provider = options.provider ?? safeProvider();
46
+ const outcome = {
47
+ ok: false,
48
+ provider,
49
+ results: [],
50
+ error: {
51
+ kind: "validation",
52
+ provider,
53
+ message: validated.message,
54
+ },
55
+ };
56
+ void emitAudit(outcome, validated.queryLength);
57
+ return errorResult(outcome);
58
+ }
59
+ const trimmedQuery = validated.query;
60
+ const maxResults = validated.maxResults;
61
+ // Resolve the active provider (Requirement 3.5: defaults to
62
+ // DuckDuckGo when no key configured).
63
+ const providerId = options.provider ?? safeProvider();
64
+ const provider = options.providerOverride ?? searchProviders[providerId];
65
+ if (!provider) {
66
+ const outcome = {
67
+ ok: false,
68
+ provider: providerId,
69
+ results: [],
70
+ error: {
71
+ kind: "validation",
72
+ provider: providerId,
73
+ message: `Unknown search provider "${providerId}". Set a supported provider via \`clai search-provider <id>\`.`,
74
+ },
75
+ };
76
+ void emitAudit(outcome, trimmedQuery.length);
77
+ return errorResult(outcome);
78
+ }
79
+ // Resolve API key (env > keychain > fallback file).
80
+ let apiKey;
81
+ if (provider.needsApiKey) {
82
+ apiKey = await (options.resolveKey
83
+ ? options.resolveKey(providerId)
84
+ : (await getSearchProviderKey(providerId)).value);
85
+ if (!apiKey || apiKey.length === 0) {
86
+ const outcome = {
87
+ ok: false,
88
+ provider: providerId,
89
+ results: [],
90
+ error: {
91
+ kind: "missing-key",
92
+ provider: providerId,
93
+ message: `${provider.displayName} requires an API key. Run \`clai set ${providerId} <KEY>\`.`,
94
+ },
95
+ };
96
+ void emitAudit(outcome, trimmedQuery.length);
97
+ return errorResult(outcome);
98
+ }
99
+ }
100
+ // Arm the 15-second invocation timeout (Requirement 1.8) and
101
+ // propagate any caller-supplied AbortSignal so SIGINT etc. still
102
+ // collapse the in-flight request.
103
+ const timeoutMs = options.timeoutMs ?? SEARCH_TIMEOUT_MS;
104
+ const controller = new AbortController();
105
+ const onCallerAbort = () => controller.abort();
106
+ if (options.signal) {
107
+ if (options.signal.aborted)
108
+ controller.abort();
109
+ else
110
+ options.signal.addEventListener("abort", onCallerAbort);
111
+ }
112
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
113
+ // Do not pin the event loop on the timeout.
114
+ timer.unref?.();
115
+ // Dispatch (Requirement 6.7: exactly one attempt, no retry).
116
+ let raw;
117
+ try {
118
+ raw = await provider.search(trimmedQuery, maxResults, { ...(apiKey !== undefined ? { apiKey } : {}) }, controller.signal);
119
+ }
120
+ catch (err) {
121
+ clearTimeout(timer);
122
+ if (options.signal)
123
+ options.signal.removeEventListener("abort", onCallerAbort);
124
+ const outcome = controller.signal.aborted
125
+ ? buildTimeoutOutcome(provider.id, timeoutMs)
126
+ : buildNetworkOutcome(provider.id, err);
127
+ void emitAudit(outcome, trimmedQuery.length);
128
+ return errorResult(outcome);
129
+ }
130
+ finally {
131
+ clearTimeout(timer);
132
+ if (options.signal)
133
+ options.signal.removeEventListener("abort", onCallerAbort);
134
+ }
135
+ // Map provider HTTP status to a categorical WebSearchErrorKind.
136
+ const httpError = classifyHttpStatus(provider.id, raw);
137
+ if (httpError) {
138
+ const outcome = {
139
+ ok: false,
140
+ provider: provider.id,
141
+ results: [],
142
+ error: httpError,
143
+ };
144
+ void emitAudit(outcome, trimmedQuery.length);
145
+ return errorResult(outcome);
146
+ }
147
+ // 2xx with parseError surfaces as a `parse` failure (Requirement 6.5).
148
+ if (raw.parseError) {
149
+ const outcome = {
150
+ ok: false,
151
+ provider: provider.id,
152
+ results: [],
153
+ error: {
154
+ kind: "parse",
155
+ provider: provider.id,
156
+ message: `${provider.displayName}: response parse error (${raw.parseError})`,
157
+ },
158
+ };
159
+ void emitAudit(outcome, trimmedQuery.length);
160
+ return errorResult(outcome);
161
+ }
162
+ // Filter and validate hits per Requirement 7.3, then truncate.
163
+ const filtered = [];
164
+ for (const hit of raw.hits) {
165
+ if (filtered.length >= maxResults)
166
+ break;
167
+ const normalised = normaliseHit(hit);
168
+ if (!normalised)
169
+ continue;
170
+ filtered.push(normalised);
171
+ }
172
+ const outcome = {
173
+ ok: true,
174
+ provider: provider.id,
175
+ results: filtered,
176
+ };
177
+ void emitAudit(outcome, trimmedQuery.length);
178
+ return successResult(outcome);
179
+ }
180
+ /**
181
+ * Synchronous validation of {@link WebSearchArgs} per Requirements 1.1,
182
+ * 1.2, 1.5, 1.6. Returns the trimmed query and a concrete `maxResults`
183
+ * value so downstream code does not need to re-derive defaults.
184
+ */
185
+ function validateArgs(args) {
186
+ const rawQuery = args?.query;
187
+ if (typeof rawQuery !== "string") {
188
+ return {
189
+ ok: false,
190
+ message: "query must be a string",
191
+ queryLength: 0,
192
+ };
193
+ }
194
+ const trimmed = rawQuery.trim();
195
+ const len = trimmed.length;
196
+ if (len < MIN_QUERY_LENGTH || len > MAX_QUERY_LENGTH) {
197
+ return {
198
+ ok: false,
199
+ message: `query length must be between ${MIN_QUERY_LENGTH} and ${MAX_QUERY_LENGTH} characters after trimming (got ${len})`,
200
+ queryLength: len,
201
+ };
202
+ }
203
+ let maxResults = DEFAULT_MAX_RESULTS;
204
+ if (args.maxResults !== undefined) {
205
+ if (typeof args.maxResults !== "number" ||
206
+ !Number.isInteger(args.maxResults) ||
207
+ args.maxResults < MIN_MAX_RESULTS ||
208
+ args.maxResults > MAX_MAX_RESULTS) {
209
+ return {
210
+ ok: false,
211
+ message: `maxResults must be an integer in [${MIN_MAX_RESULTS}, ${MAX_MAX_RESULTS}]`,
212
+ queryLength: len,
213
+ };
214
+ }
215
+ maxResults = args.maxResults;
216
+ }
217
+ return { ok: true, query: trimmed, maxResults, queryLength: len };
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Hit normalisation (Requirement 7.3)
221
+ // ---------------------------------------------------------------------------
222
+ const URL_INVALID_CHARS_RE = /[\s\u0000-\u001f\u007f]/;
223
+ function normaliseHit(hit) {
224
+ const url = typeof hit.url === "string" ? hit.url : "";
225
+ if (url.length === 0)
226
+ return undefined;
227
+ if (URL_INVALID_CHARS_RE.test(url))
228
+ return undefined;
229
+ let parsed;
230
+ try {
231
+ parsed = new URL(url);
232
+ }
233
+ catch {
234
+ return undefined;
235
+ }
236
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
237
+ return undefined;
238
+ }
239
+ const title = typeof hit.title === "string" ? hit.title.trim() : "";
240
+ if (title.length === 0)
241
+ return undefined;
242
+ const clampedTitle = title.slice(0, MAX_TITLE_LENGTH);
243
+ const snippet = typeof hit.snippet === "string" ? hit.snippet : "";
244
+ const clampedSnippet = snippet.slice(0, MAX_SNIPPET_LENGTH);
245
+ return {
246
+ title: clampedTitle,
247
+ url,
248
+ snippet: clampedSnippet,
249
+ };
250
+ }
251
+ // ---------------------------------------------------------------------------
252
+ // Error helpers
253
+ // ---------------------------------------------------------------------------
254
+ /**
255
+ * Map provider HTTP status codes to a {@link WebSearchErrorKind} per the
256
+ * design's error matrix. Returns `undefined` when the status is in the
257
+ * 2xx range so the caller can move on to body classification.
258
+ */
259
+ function classifyHttpStatus(id, raw) {
260
+ const provider = searchProviders[id];
261
+ const displayName = provider?.displayName ?? id;
262
+ const { status } = raw;
263
+ if (status >= 200 && status < 300)
264
+ return undefined;
265
+ if (status === 401 || status === 403) {
266
+ return {
267
+ kind: "auth",
268
+ provider: id,
269
+ status,
270
+ message: `${displayName} authentication failed (HTTP ${status}). Run \`clai set ${id}\` to update the key.`,
271
+ };
272
+ }
273
+ if (status === 429) {
274
+ return {
275
+ kind: "rate-limit",
276
+ provider: id,
277
+ status,
278
+ message: `${displayName} rate-limited (HTTP 429). Retry later.`,
279
+ };
280
+ }
281
+ if (status >= 500 && status < 600) {
282
+ return {
283
+ kind: "server",
284
+ provider: id,
285
+ status,
286
+ message: `${displayName} server error (HTTP ${status}).`,
287
+ };
288
+ }
289
+ if (status === 0) {
290
+ return {
291
+ kind: "network",
292
+ provider: id,
293
+ message: `${displayName}: provider returned no response (status=0).`,
294
+ };
295
+ }
296
+ return {
297
+ kind: "http",
298
+ provider: id,
299
+ status,
300
+ message: `${displayName}: HTTP ${status}.`,
301
+ };
302
+ }
303
+ function buildTimeoutOutcome(id, timeoutMs) {
304
+ const display = searchProviders[id]?.displayName ?? id;
305
+ return {
306
+ ok: false,
307
+ provider: id,
308
+ results: [],
309
+ error: {
310
+ kind: "timeout",
311
+ provider: id,
312
+ message: `${display}: timeout after ${Math.round(timeoutMs / 1000)}s`,
313
+ },
314
+ };
315
+ }
316
+ function buildNetworkOutcome(id, err) {
317
+ const display = searchProviders[id]?.displayName ?? id;
318
+ const detail = err instanceof Error ? err.message : String(err);
319
+ return {
320
+ ok: false,
321
+ provider: id,
322
+ results: [],
323
+ error: {
324
+ kind: "network",
325
+ provider: id,
326
+ message: `${display}: network failure (${detail})`,
327
+ },
328
+ };
329
+ }
330
+ // ---------------------------------------------------------------------------
331
+ // Audit + ToolResult
332
+ // ---------------------------------------------------------------------------
333
+ async function emitAudit(outcome, queryLength) {
334
+ try {
335
+ await auditLog("tool.web_search", buildSearchAuditPayload(outcome, queryLength));
336
+ }
337
+ catch {
338
+ // never let audit failures bubble up
339
+ }
340
+ }
341
+ /**
342
+ * Best-effort active-provider lookup that never throws even when the
343
+ * config store is mid-migration or unreadable. Falls back to
344
+ * `"duckduckgo"` so a fresh install still works keylessly per
345
+ * Requirement 3.5.
346
+ */
347
+ function safeProvider() {
348
+ try {
349
+ return getActiveSearchProvider();
350
+ }
351
+ catch {
352
+ return "duckduckgo";
353
+ }
354
+ }
355
+ /**
356
+ * Compose a successful {@link ToolResult}. The output starts with a one
357
+ * line summary so the agent sees the result count and provider before
358
+ * the JSON, then includes the structured `{results: [...]}` block.
359
+ */
360
+ function successResult(outcome) {
361
+ if (outcome.results.length === 0) {
362
+ // Requirement 1.7 / 7.4: literal "No results found." string.
363
+ return {
364
+ ok: true,
365
+ output: "No results found.",
366
+ exitCode: 0,
367
+ };
368
+ }
369
+ const summary = `${outcome.provider}: ${outcome.results.length} result${outcome.results.length === 1 ? "" : "s"}`;
370
+ const json = JSON.stringify({ results: outcome.results }, null, 2);
371
+ return {
372
+ ok: true,
373
+ output: `${summary}\n\n${json}`,
374
+ exitCode: 0,
375
+ };
376
+ }
377
+ function errorResult(outcome) {
378
+ const head = outcome.error?.message ?? "web.search failed";
379
+ const json = JSON.stringify({
380
+ error: outcome.error,
381
+ provider: outcome.provider,
382
+ }, null, 2);
383
+ return {
384
+ ok: false,
385
+ output: `${head}\n\n${json}`,
386
+ exitCode: 1,
387
+ };
388
+ }
389
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/tools/web/search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SSRF guard — classifies an address (or a hostname literal) as belonging
3
+ * to a private/loopback/link-local/cloud-metadata/CGNAT range.
4
+ *
5
+ * The structured {@link classify} / {@link classifyHost} helpers are used
6
+ * by the `web.fetch` pipeline (synchronous classifier branch + per-hop
7
+ * resolution check) and by the safety classifier in
8
+ * `src/safety/classifier.ts`.
9
+ *
10
+ * The boolean {@link isBlockedAddress} re-export preserves the legacy shape
11
+ * used by `src/tools/http.ts` so existing `http.fetch` semantics are
12
+ * unchanged.
13
+ *
14
+ * Per the design (see `.kiro/specs/web-search-and-fetch/design.md`,
15
+ * "Sequencing of changes" step 2 and "Safety classifier integration"), this
16
+ * module is the single source of truth for address classification — both
17
+ * the legacy `http.fetch` SSRF check and the new `web.fetch` checks
18
+ * delegate here.
19
+ */
20
+ /**
21
+ * Categorical address class returned by {@link classify} and
22
+ * {@link classifyHost} when the input belongs to a non-public range.
23
+ *
24
+ * Mappings:
25
+ * - `loopback` — 127.0.0.0/8, IPv6 `::1`, and the
26
+ * `localhost` / `ip6-localhost` hostname literals.
27
+ * - `rfc1918` — 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and
28
+ * IPv6 ULA `fc00::/7` (the IPv6 private-use range,
29
+ * grouped here as RFC1918's IPv6 cousin).
30
+ * - `ipv4-link-local` — 169.254.0.0/16 except the cloud-metadata literal.
31
+ * - `ipv6-link-local` — `fe80::/10`.
32
+ * - `cloud-metadata` — `169.254.169.254` (IPv4 EC2/AWS, GCP, Azure
33
+ * metadata) and `fd00:ec2::254` (IPv6 EC2 metadata).
34
+ * - `cgnat` — 100.64.0.0/10 (RFC 6598 carrier-grade NAT).
35
+ */
36
+ export type AddressClass = "loopback" | "rfc1918" | "ipv4-link-local" | "ipv6-link-local" | "cloud-metadata" | "cgnat";
37
+ /** Result shape returned by {@link classify} and {@link classifyHost}. */
38
+ export interface AddressClassification {
39
+ class: AddressClass;
40
+ }
41
+ /**
42
+ * Return `true` iff `url` parses as an absolute URL whose scheme is exactly
43
+ * `http:` or `https:`. Returns `false` for any other scheme (including
44
+ * `ftp:`, `file:`, `data:`, `javascript:`, `gopher:`), for empty strings,
45
+ * for malformed inputs that fail `new URL()` parsing, and for non-string
46
+ * inputs.
47
+ *
48
+ * This is the synchronous half of the "scheme allowlist" check enforced
49
+ * by the `web.fetch` pipeline (see Requirement 5.4 / 2.1). It is the
50
+ * single source of truth for the scheme allow-list so the safety
51
+ * classifier branch and the fetch-core argument validator stay in sync.
52
+ */
53
+ export declare function isAllowedScheme(url: string): boolean;
54
+ /**
55
+ * Classify a literal IP address (IPv4 or IPv6).
56
+ *
57
+ * Returns `{ class }` when the address falls into one of the
58
+ * {@link AddressClass} buckets, or `null` when the input is a
59
+ * globally-routable address (or fails to parse as either family).
60
+ *
61
+ * The check is fully synchronous and never performs DNS.
62
+ */
63
+ export declare function classify(ip: string): AddressClassification | null;
64
+ /**
65
+ * Classify a hostname or IP literal without performing DNS resolution.
66
+ *
67
+ * - For literal IPv4/IPv6 addresses (with or without surrounding `[…]`
68
+ * brackets), this delegates to {@link classify}.
69
+ * - For known-bad hostname literals (`localhost`, `localhost.localdomain`,
70
+ * `ip6-localhost`, `ip6-loopback`), this returns `{ class: "loopback" }`.
71
+ * - For every other hostname, this returns `null` (the caller is
72
+ * responsible for the DNS-resolved second pass; see
73
+ * `src/tools/web/fetch-core.ts`).
74
+ */
75
+ export declare function classifyHost(hostname: string): AddressClassification | null;
76
+ /**
77
+ * Legacy boolean shape preserved for the existing `http.fetch` SSRF check.
78
+ *
79
+ * Returns `true` whenever {@link classifyHost} would return a non-null
80
+ * classification, plus a small additional set of historically-blocked
81
+ * ranges (currently `0.0.0.0/8`, the "this network" range) that are kept
82
+ * for backward compatibility with the previous `http.ts` implementation
83
+ * but are not enumerated in the public {@link AddressClass} list.
84
+ */
85
+ export declare function isBlockedAddress(host: string): boolean;