@fgv/ts-extras 5.1.0-45 → 5.1.0-47

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 (141) hide show
  1. package/README.md +90 -0
  2. package/dist/index.browser.js +3 -1
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.js +2 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/packlets/ai-assist/index.js +1 -1
  7. package/dist/packlets/ai-assist/index.js.map +1 -1
  8. package/dist/packlets/ai-assist/jsonResponse.js +270 -7
  9. package/dist/packlets/ai-assist/jsonResponse.js.map +1 -1
  10. package/dist/packlets/ai-assist/model.js +14 -0
  11. package/dist/packlets/ai-assist/model.js.map +1 -1
  12. package/dist/packlets/ai-assist/registry.js +57 -14
  13. package/dist/packlets/ai-assist/registry.js.map +1 -1
  14. package/dist/packlets/safer-fetch/addressClassification.js +438 -0
  15. package/dist/packlets/safer-fetch/addressClassification.js.map +1 -0
  16. package/dist/packlets/safer-fetch/addressPolicy.js +119 -0
  17. package/dist/packlets/safer-fetch/addressPolicy.js.map +1 -0
  18. package/dist/packlets/safer-fetch/contentType.js +120 -0
  19. package/dist/packlets/safer-fetch/contentType.js.map +1 -0
  20. package/dist/packlets/safer-fetch/deadline.js +254 -0
  21. package/dist/packlets/safer-fetch/deadline.js.map +1 -0
  22. package/dist/packlets/safer-fetch/defaults.js +131 -0
  23. package/dist/packlets/safer-fetch/defaults.js.map +1 -0
  24. package/dist/packlets/safer-fetch/failureReason.js +21 -0
  25. package/dist/packlets/safer-fetch/failureReason.js.map +1 -0
  26. package/dist/packlets/safer-fetch/guards.js +148 -0
  27. package/dist/packlets/safer-fetch/guards.js.map +1 -0
  28. package/dist/packlets/safer-fetch/index.browser.js +58 -0
  29. package/dist/packlets/safer-fetch/index.browser.js.map +1 -0
  30. package/dist/packlets/safer-fetch/index.js +52 -0
  31. package/dist/packlets/safer-fetch/index.js.map +1 -0
  32. package/dist/packlets/safer-fetch/model.js +21 -0
  33. package/dist/packlets/safer-fetch/model.js.map +1 -0
  34. package/dist/packlets/safer-fetch/nodeAddressGuard.js +191 -0
  35. package/dist/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  36. package/dist/packlets/safer-fetch/redirect.js +93 -0
  37. package/dist/packlets/safer-fetch/redirect.js.map +1 -0
  38. package/dist/packlets/safer-fetch/retry.js +188 -0
  39. package/dist/packlets/safer-fetch/retry.js.map +1 -0
  40. package/dist/packlets/safer-fetch/saferFetch.js +755 -0
  41. package/dist/packlets/safer-fetch/saferFetch.js.map +1 -0
  42. package/dist/packlets/safer-fetch/transport.js +50 -0
  43. package/dist/packlets/safer-fetch/transport.js.map +1 -0
  44. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  45. package/dist/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  46. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js +16 -1
  47. package/dist/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  48. package/dist/ts-extras.d.ts +1590 -13
  49. package/lib/index.browser.d.ts +2 -1
  50. package/lib/index.browser.d.ts.map +1 -1
  51. package/lib/index.browser.js +4 -1
  52. package/lib/index.browser.js.map +1 -1
  53. package/lib/index.d.ts +2 -1
  54. package/lib/index.d.ts.map +1 -1
  55. package/lib/index.js +3 -1
  56. package/lib/index.js.map +1 -1
  57. package/lib/packlets/ai-assist/index.d.ts +1 -1
  58. package/lib/packlets/ai-assist/index.d.ts.map +1 -1
  59. package/lib/packlets/ai-assist/index.js +3 -2
  60. package/lib/packlets/ai-assist/index.js.map +1 -1
  61. package/lib/packlets/ai-assist/jsonResponse.d.ts +96 -0
  62. package/lib/packlets/ai-assist/jsonResponse.d.ts.map +1 -1
  63. package/lib/packlets/ai-assist/jsonResponse.js +271 -7
  64. package/lib/packlets/ai-assist/jsonResponse.js.map +1 -1
  65. package/lib/packlets/ai-assist/model.d.ts +43 -0
  66. package/lib/packlets/ai-assist/model.d.ts.map +1 -1
  67. package/lib/packlets/ai-assist/model.js +14 -0
  68. package/lib/packlets/ai-assist/model.js.map +1 -1
  69. package/lib/packlets/ai-assist/registry.d.ts +26 -6
  70. package/lib/packlets/ai-assist/registry.d.ts.map +1 -1
  71. package/lib/packlets/ai-assist/registry.js +57 -14
  72. package/lib/packlets/ai-assist/registry.js.map +1 -1
  73. package/lib/packlets/safer-fetch/addressClassification.d.ts +144 -0
  74. package/lib/packlets/safer-fetch/addressClassification.d.ts.map +1 -0
  75. package/lib/packlets/safer-fetch/addressClassification.js +441 -0
  76. package/lib/packlets/safer-fetch/addressClassification.js.map +1 -0
  77. package/lib/packlets/safer-fetch/addressPolicy.d.ts +113 -0
  78. package/lib/packlets/safer-fetch/addressPolicy.d.ts.map +1 -0
  79. package/lib/packlets/safer-fetch/addressPolicy.js +123 -0
  80. package/lib/packlets/safer-fetch/addressPolicy.js.map +1 -0
  81. package/lib/packlets/safer-fetch/contentType.d.ts +51 -0
  82. package/lib/packlets/safer-fetch/contentType.d.ts.map +1 -0
  83. package/lib/packlets/safer-fetch/contentType.js +127 -0
  84. package/lib/packlets/safer-fetch/contentType.js.map +1 -0
  85. package/lib/packlets/safer-fetch/deadline.d.ts +138 -0
  86. package/lib/packlets/safer-fetch/deadline.d.ts.map +1 -0
  87. package/lib/packlets/safer-fetch/deadline.js +258 -0
  88. package/lib/packlets/safer-fetch/deadline.js.map +1 -0
  89. package/lib/packlets/safer-fetch/defaults.d.ts +108 -0
  90. package/lib/packlets/safer-fetch/defaults.d.ts.map +1 -0
  91. package/lib/packlets/safer-fetch/defaults.js +134 -0
  92. package/lib/packlets/safer-fetch/defaults.js.map +1 -0
  93. package/lib/packlets/safer-fetch/failureReason.d.ts +148 -0
  94. package/lib/packlets/safer-fetch/failureReason.d.ts.map +1 -0
  95. package/lib/packlets/safer-fetch/failureReason.js +22 -0
  96. package/lib/packlets/safer-fetch/failureReason.js.map +1 -0
  97. package/lib/packlets/safer-fetch/guards.d.ts +62 -0
  98. package/lib/packlets/safer-fetch/guards.d.ts.map +1 -0
  99. package/lib/packlets/safer-fetch/guards.js +153 -0
  100. package/lib/packlets/safer-fetch/guards.js.map +1 -0
  101. package/lib/packlets/safer-fetch/index.browser.d.ts +33 -0
  102. package/lib/packlets/safer-fetch/index.browser.d.ts.map +1 -0
  103. package/lib/packlets/safer-fetch/index.browser.js +82 -0
  104. package/lib/packlets/safer-fetch/index.browser.js.map +1 -0
  105. package/lib/packlets/safer-fetch/index.d.ts +27 -0
  106. package/lib/packlets/safer-fetch/index.d.ts.map +1 -0
  107. package/lib/packlets/safer-fetch/index.js +78 -0
  108. package/lib/packlets/safer-fetch/index.js.map +1 -0
  109. package/lib/packlets/safer-fetch/model.d.ts +373 -0
  110. package/lib/packlets/safer-fetch/model.d.ts.map +1 -0
  111. package/lib/packlets/safer-fetch/model.js +22 -0
  112. package/lib/packlets/safer-fetch/model.js.map +1 -0
  113. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts +129 -0
  114. package/lib/packlets/safer-fetch/nodeAddressGuard.d.ts.map +1 -0
  115. package/lib/packlets/safer-fetch/nodeAddressGuard.js +196 -0
  116. package/lib/packlets/safer-fetch/nodeAddressGuard.js.map +1 -0
  117. package/lib/packlets/safer-fetch/redirect.d.ts +62 -0
  118. package/lib/packlets/safer-fetch/redirect.d.ts.map +1 -0
  119. package/lib/packlets/safer-fetch/redirect.js +98 -0
  120. package/lib/packlets/safer-fetch/redirect.js.map +1 -0
  121. package/lib/packlets/safer-fetch/retry.d.ts +162 -0
  122. package/lib/packlets/safer-fetch/retry.d.ts.map +1 -0
  123. package/lib/packlets/safer-fetch/retry.js +197 -0
  124. package/lib/packlets/safer-fetch/retry.js.map +1 -0
  125. package/lib/packlets/safer-fetch/saferFetch.d.ts +108 -0
  126. package/lib/packlets/safer-fetch/saferFetch.d.ts.map +1 -0
  127. package/lib/packlets/safer-fetch/saferFetch.js +760 -0
  128. package/lib/packlets/safer-fetch/saferFetch.js.map +1 -0
  129. package/lib/packlets/safer-fetch/transport.d.ts +18 -0
  130. package/lib/packlets/safer-fetch/transport.d.ts.map +1 -0
  131. package/lib/packlets/safer-fetch/transport.js +53 -0
  132. package/lib/packlets/safer-fetch/transport.js.map +1 -0
  133. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts +54 -6
  134. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.d.ts.map +1 -1
  135. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js +65 -7
  136. package/lib/packlets/zip-file-tree/zipFileTreeAccessors.js.map +1 -1
  137. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts +26 -1
  138. package/lib/packlets/zip-file-tree/zipFileTreeWriter.d.ts.map +1 -1
  139. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js +17 -1
  140. package/lib/packlets/zip-file-tree/zipFileTreeWriter.js.map +1 -1
  141. package/package.json +17 -7
@@ -2,6 +2,7 @@ import { Brand } from '@fgv/ts-utils';
2
2
  import { Conversion } from '@fgv/ts-utils';
3
3
  import { Converter } from '@fgv/ts-utils';
4
4
  import { DateTime } from 'luxon';
5
+ import { DetailedResult } from '@fgv/ts-utils';
5
6
  import { FileTree } from '@fgv/ts-json-base';
6
7
  import { Hash as Hash_2 } from '@fgv/ts-utils';
7
8
  import { JsonObject } from '@fgv/ts-json-base';
@@ -12,6 +13,54 @@ import { Result } from '@fgv/ts-utils';
12
13
  import { Uuid } from '@fgv/ts-utils';
13
14
  import { Validator } from '@fgv/ts-utils';
14
15
 
16
+ /**
17
+ * The security-relevant classification of an IP address.
18
+ *
19
+ * `'public'` is the only classification that describes an address which is
20
+ * globally routable on the public internet; every other value names a
21
+ * special-purpose range that a server-side fetch reaching untrusted input
22
+ * should not be able to reach by default.
23
+ *
24
+ * @public
25
+ */
26
+ declare type AddressClassification =
27
+ /** Globally routable unicast — the only classification a default guard permits. */
28
+ 'public'
29
+ /** `0.0.0.0/8` and `::` — "this host on this network"; routes to localhost on Linux. */
30
+ | 'unspecified'
31
+ /** `127.0.0.0/8` and `::1`. */
32
+ | 'loopback'
33
+ /**
34
+ * `169.254.0.0/16` and `fe80::/10`. The IPv4 range contains the cloud
35
+ * instance-metadata endpoint (`169.254.169.254`) and is the single
36
+ * highest-value SSRF target.
37
+ */
38
+ | 'link-local'
39
+ /** RFC 1918 — `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`. */
40
+ | 'private'
41
+ /** RFC 4193 IPv6 unique-local — `fc00::/7`. */
42
+ | 'unique-local'
43
+ /** RFC 6598 carrier-grade NAT — `100.64.0.0/10`. Frequently carrier or container internal. */
44
+ | 'carrier-grade-nat'
45
+ /** RFC 2544 benchmarking — `198.18.0.0/15`. */
46
+ | 'benchmarking'
47
+ /** Documentation ranges — `192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`, `2001:db8::/32`. */
48
+ | 'documentation'
49
+ /** IETF protocol assignments — `192.0.0.0/24` and `2001::/23`. */
50
+ | 'protocol-assignment'
51
+ /** `224.0.0.0/4` and `ff00::/8`. */
52
+ | 'multicast'
53
+ /** `255.255.255.255/32`. */
54
+ | 'broadcast'
55
+ /** Every other special-purpose or future-use range (e.g. `240.0.0.0/4`, `fec0::/10`). */
56
+ | 'reserved';
57
+
58
+ /**
59
+ * The address family of a parsed IP address literal.
60
+ * @public
61
+ */
62
+ declare type AddressFamily = 'ipv4' | 'ipv6';
63
+
15
64
  /**
16
65
  * Key size in bytes for AES-256.
17
66
  * @public
@@ -164,10 +213,12 @@ declare namespace AiAssist {
164
213
  modelSpecKey,
165
214
  modelSpec,
166
215
  resolveEffectiveTools,
216
+ classifyJsonParseFailure,
167
217
  extractJsonText,
168
218
  fencedStringifiedJson,
169
219
  IFencedStringifiedJsonExtractorOptions,
170
220
  IFencedStringifiedJsonOptions,
221
+ JsonParseFailureReason,
171
222
  JsonTextExtractor,
172
223
  generateJsonCompletion,
173
224
  SMART_JSON_PROMPT_HINT,
@@ -416,12 +467,89 @@ declare const allModelCapabilities: ReadonlyArray<AiModelCapability>;
416
467
  */
417
468
  declare const allModelSpecKeys: ReadonlyArray<ModelSpecKey>;
418
469
 
470
+ /**
471
+ * An address guard that permits every address.
472
+ *
473
+ * @remarks
474
+ * **This is the absence of the SSRF guarantee, given a name.** It performs no address
475
+ * classification, no DNS resolution, and no scheme or port narrowing beyond the core's
476
+ * refusal of non-`http(s)` schemes. A call site using it is reachable at any address the
477
+ * process can route to, including cloud metadata endpoints, loopback admin ports, and
478
+ * RFC-1918 hosts.
479
+ *
480
+ * It ships anyway, for two reasons. It is the honest and only possible choice in a browser,
481
+ * where neither DNS resolution nor redirect interposition exists. And omitting it would not
482
+ * make anyone safer — it would make consumers hand-roll something worse. It is deliberately
483
+ * named to be uncomfortable in review and to grep as a distinct posture.
484
+ *
485
+ * This is the **guard-layer** factory: it returns an {@link SaferFetch.IAddressGuard}, which is
486
+ * what an entry point's `addressGuard` option takes. A pure address *policy* — the synchronous,
487
+ * address-list-in classifier a resolving guard delegates to — is a different layer and needs a
488
+ * distinct name.
489
+ * @public
490
+ */
491
+ declare function allowAnyAddress(): IAddressGuard;
492
+
493
+ /**
494
+ * Creates a policy that permits every address, including loopback, link-local
495
+ * and private ones.
496
+ *
497
+ * **This policy provides no protection whatsoever.** It classifies nothing,
498
+ * rejects nothing, and never fails — not even for an empty address list or an
499
+ * address that is not a well-formed literal. It exists so that choosing to go
500
+ * without an address guarantee is a deliberate, named, greppable act at the
501
+ * call site rather than something reachable by omission, and so that tests and
502
+ * genuinely trusted-input paths do not hand-roll something worse.
503
+ *
504
+ * It is the only correct choice in a browser, where neither name resolution nor
505
+ * redirect interposition is available and no address guarantee is possible.
506
+ *
507
+ * @returns the policy.
508
+ * @public
509
+ */
510
+ declare function allowAnyAddressPolicy(): IAddressPolicy;
511
+
512
+ /**
513
+ * A response-headers guard that accepts only the listed media types.
514
+ *
515
+ * @remarks
516
+ * Rejecting on `Content-Type` is strictly cheaper than capping mid-read — a header comparison
517
+ * instead of a partial body transfer — so a consumer ingesting URLs wants it on every call. It
518
+ * is also what keeps an HTML error page served with a `200` from being parsed as JSON twenty
519
+ * frames away from where it went wrong.
520
+ *
521
+ * Entries are media types, optionally with a wildcard subtype (`text/*`); a wildcard type
522
+ * requires a wildcard subtype. Matching is case-insensitive and ignores parameters, so
523
+ * `text/html` accepts `text/html; charset=utf-8`. A response with **no** `Content-Type` is
524
+ * rejected: an untyped response has not satisfied a content-type allowlist.
525
+ *
526
+ * Construction is fallible because a malformed entry in a security-adjacent allowlist must be
527
+ * reported to its author rather than silently compiled into a pattern that never matches.
528
+ *
529
+ * @param types - Accepted media types. Must be non-empty.
530
+ * @public
531
+ */
532
+ declare function allowContentTypes(types: ReadonlyArray<string>): Result<IResponseHeadersGuard>;
533
+
419
534
  /**
420
535
  * All valid provider ID values, in the same order as the registry.
421
536
  * @public
422
537
  */
423
538
  declare const allProviderIds: ReadonlyArray<AiProviderId>;
424
539
 
540
+ /**
541
+ * Header names dropped on every cross-origin redirect hop, whatever the caller configured.
542
+ *
543
+ * @remarks
544
+ * Turning on manual redirects makes this primitive responsible for a rule the platform was
545
+ * applying for free: browsers and `curl` both strip credential headers when a redirect leaves
546
+ * the origin, and a hand-rolled loop that replays them hands `Authorization: Bearer …` to
547
+ * whatever host the redirect names. Callers add their own names with
548
+ * `ISaferFetchOptions.sensitiveHeaders`; these three are not removable.
549
+ * @public
550
+ */
551
+ declare const ALWAYS_STRIPPED_HEADERS: ReadonlyArray<string>;
552
+
425
553
  /**
426
554
  * Maps Anthropic effort level to the `thinking.budget_tokens` integer that the
427
555
  * Anthropic API requires when `thinking.type === 'enabled'`.
@@ -497,6 +625,85 @@ declare function base64UrlNoPadDecode(encoded: string): Result<Uint8Array>;
497
625
  */
498
626
  declare function base64UrlNoPadEncode(data: Uint8Array): string;
499
627
 
628
+ /**
629
+ * Creates the recommended address guard: resolves each hop's host and requires every resolved
630
+ * address to be globally routable public unicast.
631
+ *
632
+ * @remarks
633
+ * **Node only** — it resolves names, and no browser API returns a hostname's A/AAAA records.
634
+ * `allowAnyAddress()` is the honest choice there, and its name says so.
635
+ *
636
+ * This is the guard an entry point's `addressGuard` option takes. It is the resolving,
637
+ * hop-chain-aware half; the judgement itself belongs to
638
+ * {@link SaferFetch.blockPrivateNetworksPolicy}, which this guard delegates to unchanged. Keeping
639
+ * the adversarial classification matrix in one pure, synchronous implementation is what makes
640
+ * "did the address check run, and run correctly?" answerable by reading one file.
641
+ *
642
+ * The guard is invoked **once per redirect hop**, on the last entry of the chain. Hop 0 is not a
643
+ * special case: a guard that only validated the initial URL is defeated by a single `302` to
644
+ * `http://169.254.169.254/`.
645
+ *
646
+ * The URL-level constraints — `https:` unless `allowInsecureHttp`, plus the optional `allowHosts`
647
+ * and `allowPorts` allowlists — are checked **before** the name resolution, so a host the caller
648
+ * never allowlisted is refused by string comparison rather than handed to a resolver.
649
+ *
650
+ * ```typescript
651
+ * // A local Ollama sidecar: every deviation from the default posture is named and greppable.
652
+ * const guard = blockPrivateNetworks({
653
+ * allowLoopback: true,
654
+ * allowInsecureHttp: true,
655
+ * allowHosts: ['localhost'],
656
+ * allowPorts: [11434]
657
+ * });
658
+ * ```
659
+ *
660
+ * **What this does not protect against.** It validates a resolved address and the transport then
661
+ * re-resolves, so hostile DNS can answer the two lookups differently — the documented
662
+ * DNS-rebinding limit, which is open in this release. A strict `allowHosts` list is the
663
+ * recommended posture precisely because it shrinks that exposure to "an allowlisted host's own
664
+ * resolver is hostile".
665
+ *
666
+ * @param options - optional relaxations of the default posture, plus the resolver seam.
667
+ * @returns the guard. Construction cannot fail.
668
+ * @public
669
+ */
670
+ declare function blockPrivateNetworks(options?: IBlockPrivateNetworksGuardOptions): IAddressGuard;
671
+
672
+ /**
673
+ * Creates the recommended address policy: every address must be a globally
674
+ * routable public unicast address.
675
+ *
676
+ * Rejected, in both their plain and their IPv6-embedded encodings: loopback,
677
+ * the link-local range that carries the cloud instance-metadata endpoint
678
+ * (`169.254.169.254`), RFC 1918 private ranges, IPv6 unique-local, carrier-grade
679
+ * NAT (`100.64.0.0/10`), the unspecified address, multicast, broadcast,
680
+ * benchmarking, documentation and every other reserved range. The encoding
681
+ * bypasses covered are IPv4-mapped IPv6 (`::ffff:169.254.169.254`),
682
+ * IPv4-compatible IPv6, NAT64 (`64:ff9b::a9fe:a9fe`), 6to4 (`2002:a9fe:a9fe::`)
683
+ * and the shortened / octal / hexadecimal / decimal IPv4 literal forms
684
+ * (`127.1`, `0177.0.0.1`, `0x7f.1`, `2130706433`).
685
+ *
686
+ * **What this does not protect against.** The policy classifies addresses a
687
+ * caller has already resolved; it cannot see the address the connection
688
+ * ultimately uses. A hostile DNS server that answers the resolution with a
689
+ * public address and the connect with a private one is not stopped by this
690
+ * policy — closing that requires connecting to a pinned address. The policy
691
+ * also says nothing about scheme, host, port, redirects or response content.
692
+ *
693
+ * This is the **policy-layer** factory. It is not what a safer-fetch call's
694
+ * `addressGuard` option takes: an entry point takes an `IAddressGuard`, which is
695
+ * the asynchronous, hop-chain-aware, name-resolving half. `blockPrivateNetworks`
696
+ * (Node only) is the guard that resolves a hostname and delegates every resolved
697
+ * address to this policy; reach for that unless you are classifying an address
698
+ * list you already hold.
699
+ *
700
+ * @param options - optional {@link IBlockPrivateNetworksOptions | relaxations}
701
+ * of the default posture.
702
+ * @returns the policy. Construction cannot fail.
703
+ * @public
704
+ */
705
+ declare function blockPrivateNetworksPolicy(options?: IBlockPrivateNetworksOptions): IAddressPolicy;
706
+
500
707
  /**
501
708
  * Calls the appropriate chat completion API for a given provider. Routes by
502
709
  * `apiFormat`: `'openai'` (xAI/OpenAI/Groq/Mistral — switches to Responses API
@@ -636,6 +843,92 @@ declare function callProxiedImageGeneration(proxyUrl: string, params: IProviderI
636
843
  */
637
844
  declare function callProxiedListModels(proxyUrl: string, params: IProviderListModelsParams): Promise<Result<ReadonlyArray<IAiModelInfo>>>;
638
845
 
846
+ /**
847
+ * Classifies a single IP address literal.
848
+ *
849
+ * The function is pure, synchronous, and deterministic — it performs no name
850
+ * resolution and no I/O. Input that is not an IP address literal (a DNS
851
+ * hostname, for instance) fails; callers that accept hostnames should treat a
852
+ * failure here as "not a literal" and resolve the name, then classify each
853
+ * resolved address.
854
+ *
855
+ * Accepted forms:
856
+ *
857
+ * - dotted-quad IPv4 (`169.254.169.254`)
858
+ * - the shortened and non-decimal IPv4 forms the WHATWG URL parser accepts
859
+ * (`127.1`, `2130706433`, `0177.0.0.1`, `0x7f.1`)
860
+ * - IPv6, with or without `::` compression, optionally bracketed as a URL
861
+ * `hostname` is (`[::1]`) and optionally carrying a zone id (`fe80::1%eth0`)
862
+ * - IPv6 forms that embed an IPv4 address — IPv4-mapped, IPv4-compatible,
863
+ * NAT64 and 6to4 — which are classified by their embedded IPv4 address
864
+ *
865
+ * A trailing dot is accepted on an IPv4 literal (`127.0.0.1.`), matching URL
866
+ * hostname normalization.
867
+ *
868
+ * **Classify a URL's `hostname`, never the raw URL text.** This function does
869
+ * not apply IDNA/Unicode normalization, and that step is not cosmetic: the
870
+ * platform's URL parser reads `http://127.0.0.1/`, `http://127。0。0。1/` and
871
+ * even `http://⑫7.0.0.1/` as the host `127.0.0.1`, because IDNA normalizes
872
+ * fullwidth digits, the ideographic full stop and circled numbers to their
873
+ * ASCII equivalents. Handed one of those strings directly this function fails —
874
+ * which is fail-closed, but only because the caller is then expected to treat
875
+ * "not a literal" as "resolve it as a name". Reading `new URL(...).hostname`
876
+ * gets the normalization for free and is the only supported use.
877
+ *
878
+ * @param address - the address literal to classify.
879
+ * @returns `Success` with the {@link IClassifiedAddress | classification},
880
+ * or `Failure` if the supplied text is not a well-formed IP address literal.
881
+ * @public
882
+ */
883
+ declare function classifyAddress(address: string): Result<IClassifiedAddress>;
884
+
885
+ /**
886
+ * Classifies why a JSON-shaped LLM response would not parse, returning a
887
+ * {@link AiAssist.JsonParseFailureReason} a caller can branch on — repair the
888
+ * cheap cases, re-prompt the expensive ones, fail outright on the rest —
889
+ * instead of regex-matching an engine-specific `JSON.parse` message.
890
+ *
891
+ * Pass the same raw model text you handed
892
+ * {@link AiAssist.fencedStringifiedJson} or {@link AiAssist.extractJsonText};
893
+ * this applies the same BOM / whitespace / fence / preamble handling before
894
+ * scanning, and reports `offset` against that original text.
895
+ *
896
+ * Classification is **structural and deliberately conservative**. The scan
897
+ * walks the JSON grammar itself rather than reading the engine's error string,
898
+ * so its verdicts are stable across Node versions — and any fault it cannot
899
+ * name with confidence comes back as `'unknown'` rather than a guess. In
900
+ * particular an input that opened a structure and never closed it (the
901
+ * truncated-response shape {@link AiAssist.extractJsonText} already diagnoses)
902
+ * classifies as `'unknown'` here; the two diagnostics are complementary, not
903
+ * competing.
904
+ *
905
+ * This never fails and never repairs — it only names the fault. It is a
906
+ * diagnostic on the failure path, so calling it on text that parses fine is
907
+ * harmless but pointless: it returns `'unknown'`.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * const parsed = fencedStringifiedJson({ inner }).convert(raw);
912
+ * if (parsed.isFailure()) {
913
+ * const reason = classifyJsonParseFailure(raw);
914
+ * switch (reason.kind) {
915
+ * case 'unquoted-property-name': // cheap to repair
916
+ * case 'single-quoted-property-name':
917
+ * break;
918
+ * case 'elided-member':
919
+ * case 'unterminated-property-name': // worth a re-prompt
920
+ * break;
921
+ * default: // 'unknown' — fail outright
922
+ * }
923
+ * }
924
+ * ```
925
+ *
926
+ * @param text - Raw model output (the same string handed to the extractor).
927
+ * @returns A {@link AiAssist.JsonParseFailureReason}.
928
+ * @public
929
+ */
930
+ declare function classifyJsonParseFailure(text: string): JsonParseFailureReason;
931
+
639
932
  declare namespace Constants {
640
933
  export {
641
934
  ENCRYPTED_FILE_FORMAT,
@@ -710,8 +1003,22 @@ declare function createEncryptedFile<TMetadata = JsonValue>(params: ICreateEncry
710
1003
  */
711
1004
  declare function createEncryptedFileConverter<TMetadata = JsonValue>(metadataConverter?: Converter<TMetadata>): Converter<IEncryptedFile<TMetadata>>;
712
1005
 
1006
+ /**
1007
+ * Creates a zip file from an array of files whose contents are either text or raw bytes.
1008
+ *
1009
+ * @remarks
1010
+ * String contents are encoded as UTF-8; `Uint8Array` contents are stored verbatim, so
1011
+ * arbitrary binary payloads round-trip through the archive unchanged.
1012
+ * @public
1013
+ */
1014
+ declare function createZipFromFiles(files: ReadonlyArray<IZipFile>, options?: ICreateZipOptions): Result<Uint8Array>;
1015
+
713
1016
  /**
714
1017
  * Creates a zip file from an array of text files.
1018
+ *
1019
+ * @remarks
1020
+ * Contents are encoded as UTF-8. Use `createZipFromFiles` to write entries whose
1021
+ * contents are raw bytes.
715
1022
  * @public
716
1023
  */
717
1024
  declare function createZipFromTextFiles(files: ReadonlyArray<IZipTextFile>, options?: ICreateZipOptions): Result<Uint8Array>;
@@ -842,6 +1149,13 @@ declare const DEFAULT_ALGORITHM: "AES-256-GCM";
842
1149
  */
843
1150
  declare const DEFAULT_ANTHROPIC_MAX_TOKENS: number;
844
1151
 
1152
+ /**
1153
+ * Default deadline to first response headers, in milliseconds. A host that has not sent
1154
+ * headers in ten seconds is not answering.
1155
+ * @public
1156
+ */
1157
+ declare const DEFAULT_HEADERS_TIMEOUT_MS: number;
1158
+
845
1159
  /**
846
1160
  * Default PBKDF2 iterations for key store encryption.
847
1161
  * Higher than regular files since this protects the master key vault.
@@ -849,6 +1163,29 @@ declare const DEFAULT_ANTHROPIC_MAX_TOKENS: number;
849
1163
  */
850
1164
  declare const DEFAULT_KEYSTORE_ITERATIONS: number;
851
1165
 
1166
+ /**
1167
+ * Default cap on redirect hops followed under `'validate-each-hop'`.
1168
+ *
1169
+ * @remarks
1170
+ * Five is enough for the ordinary shortener-then-canonicalize-then-CDN chains real services
1171
+ * produce and small enough that a chain designed to burn budget is refused quickly. Every hop
1172
+ * costs a full guard evaluation — including a DNS resolution — against the same overall
1173
+ * deadline, so the cap bounds work, not just politeness.
1174
+ * @public
1175
+ */
1176
+ declare const DEFAULT_MAX_REDIRECTS: number;
1177
+
1178
+ /**
1179
+ * Default cap on decoded response bytes (5 MiB) — roughly an order of magnitude above a
1180
+ * realistic JSON API response and well below a size that threatens a Node heap.
1181
+ *
1182
+ * @remarks
1183
+ * Raising it is a per-call option, not a construction-time setting, precisely because real
1184
+ * documents clear this. See `ISaferFetchOptions.maxResponseBytes`.
1185
+ * @public
1186
+ */
1187
+ declare const DEFAULT_MAX_RESPONSE_BYTES: number;
1188
+
852
1189
  /**
853
1190
  * Default capability config used by `callProviderListModels` when callers
854
1191
  * don't supply their own. Patterns are intentionally narrow — false
@@ -866,6 +1203,23 @@ declare const DEFAULT_MODEL_CAPABILITY_CONFIG: IAiModelCapabilityConfig;
866
1203
  */
867
1204
  declare const DEFAULT_RANGEOF_FORMATS: RangeOfFormats;
868
1205
 
1206
+ /**
1207
+ * Default base delay for {@link SaferFetch.IRetryPolicy | retry} backoff, in milliseconds.
1208
+ * @public
1209
+ */
1210
+ declare const DEFAULT_RETRY_BASE_DELAY_MS: number;
1211
+
1212
+ /**
1213
+ * Default ceiling on a single {@link SaferFetch.IRetryPolicy | retry} delay, in milliseconds.
1214
+ *
1215
+ * @remarks
1216
+ * It bounds the computed backoff **and** a server-supplied `Retry-After`. That second role is
1217
+ * the security-relevant one: `Retry-After` is a header an attacker-controlled server chooses
1218
+ * freely, so an unclamped `Retry-After: 86400` is a denial of service on the caller.
1219
+ * @public
1220
+ */
1221
+ declare const DEFAULT_RETRY_MAX_DELAY_MS: number;
1222
+
869
1223
  /**
870
1224
  * Default PBKDF2 iterations for secret-level key derivation.
871
1225
  * Lower than keystore encryption since these are used more frequently.
@@ -873,6 +1227,14 @@ declare const DEFAULT_RANGEOF_FORMATS: RangeOfFormats;
873
1227
  */
874
1228
  declare const DEFAULT_SECRET_ITERATIONS: number;
875
1229
 
1230
+ /**
1231
+ * Default overall deadline, in milliseconds — comfortably above a slow-but-real API and far
1232
+ * below "a hung request pins a socket for minutes". Streaming LLM calls should not use this
1233
+ * primitive at all; that is what `AiAssist` is for.
1234
+ * @public
1235
+ */
1236
+ declare const DEFAULT_TIMEOUT_MS: number;
1237
+
876
1238
  /**
877
1239
  * Derives an asymmetric keypair *deterministically* from a fixed secret seed
878
1240
  * using `globalThis.crypto.subtle`, shared by the Node and browser
@@ -1292,6 +1654,155 @@ declare function fencedStringifiedJson(options?: IFencedStringifiedJsonExtractor
1292
1654
  */
1293
1655
  declare function fencedStringifiedJson<T>(options: IFencedStringifiedJsonOptions<T>): Converter<T>;
1294
1656
 
1657
+ /**
1658
+ * Machine-readable reason a safer-fetch call failed, carried as the detail of the
1659
+ * `DetailedResult` every entry point returns.
1660
+ *
1661
+ * @remarks
1662
+ * **This value is an internal-network scanning oracle. Do not echo it, or any string
1663
+ * derived from it, to an untrusted caller.** A `'blocked-by-guard'` detail names the
1664
+ * URL, the hop, and the guard that rejected it; an attacker who can influence the
1665
+ * request URL and observe the error learns whether an internal host exists. Log the
1666
+ * detail; return a coarse code (the `kind` alone, or a generic message) to callers.
1667
+ *
1668
+ * Following the `AiAssist.JsonParseFailureReason` precedent in this package: a `kind`
1669
+ * discriminant with per-kind payload fields, and `'unknown'` as an honest catch-all
1670
+ * rather than a guess.
1671
+ * @public
1672
+ */
1673
+ declare type FetchFailureReason =
1674
+ /** The supplied URL could not be parsed, or its scheme is not `http:` or `https:`. */
1675
+ {
1676
+ readonly kind: 'invalid-url';
1677
+ readonly url: string;
1678
+ readonly detail: string;
1679
+ }
1680
+ /**
1681
+ * A guard refused. `hop` is 0-based; hop 0 is the caller's URL, and `guard` names which
1682
+ * guard rejected — which is what makes "did the address check run, and was it the thing that
1683
+ * said no?" answerable from the failure alone. A content-type rejection is reported as
1684
+ * `'unsupported-content-type'` instead, because that case carries a more useful payload.
1685
+ */
1686
+ | {
1687
+ readonly kind: 'blocked-by-guard';
1688
+ readonly url: string;
1689
+ readonly hop: number;
1690
+ readonly guard: string;
1691
+ readonly detail: string;
1692
+ }
1693
+ /**
1694
+ * A redirect status was received that this call would not follow: the policy is `'reject'`,
1695
+ * or the response carried no usable `Location`, or following it would revisit a URL already
1696
+ * in the chain.
1697
+ *
1698
+ * @remarks
1699
+ * The three are deliberately one kind. Splitting them would buy a caller almost nothing and
1700
+ * would widen the scanning oracle this taxonomy already is — and the two follow-time cases
1701
+ * are facts about the chain the redirecting server produced, not about the network behind
1702
+ * this process. "The chain got too long" is a different question and stays
1703
+ * `'too-many-redirects'`.
1704
+ *
1705
+ * `url` is always the URL that **issued** the rejected redirect — the hop this call actually
1706
+ * requested and got a 3xx back from — never the `Location` target it pointed at. That holds
1707
+ * for all three cases, including the revisit case, where the target is the URL already in the
1708
+ * chain and naming it here would make the same field mean two different things. The target is
1709
+ * named in the message instead. `status` is the redirect status that was received.
1710
+ */
1711
+ | {
1712
+ readonly kind: 'redirect-rejected';
1713
+ readonly url: string;
1714
+ readonly status: number;
1715
+ }
1716
+ /**
1717
+ * The platform returned an opaque redirect, whose `Location` is not readable. This is
1718
+ * what a browser yields for `redirect: 'manual'`; the hop cannot be inspected at all.
1719
+ */
1720
+ | {
1721
+ readonly kind: 'redirect-opaque';
1722
+ }
1723
+ /** The redirect hop budget was exhausted. */
1724
+ | {
1725
+ readonly kind: 'too-many-redirects';
1726
+ readonly hops: number;
1727
+ readonly limit: number;
1728
+ }
1729
+ /** One of the deadlines elapsed. Distinct from `'aborted'`, which is the caller's signal. */
1730
+ | {
1731
+ readonly kind: 'timeout';
1732
+ readonly phase: FetchTimeoutPhase;
1733
+ readonly elapsedMs: number;
1734
+ readonly limitMs: number;
1735
+ }
1736
+ /** The caller's `AbortSignal` fired. Distinct from `'timeout'`, which is our deadline. */
1737
+ | {
1738
+ readonly kind: 'aborted';
1739
+ }
1740
+ /** The transport could not complete the request. */
1741
+ | {
1742
+ readonly kind: 'network';
1743
+ readonly detail: string;
1744
+ }
1745
+ /**
1746
+ * A non-2xx response. `bodyPreview` is **never populated in this release** — error bodies
1747
+ * routinely echo request content, including credentials, so surfacing one has to be an
1748
+ * explicit, length-capped opt-in rather than a default. The field is declared so that adding
1749
+ * that opt-in later is additive.
1750
+ */
1751
+ | {
1752
+ readonly kind: 'http-status';
1753
+ readonly status: number;
1754
+ readonly statusText: string;
1755
+ readonly bodyPreview?: string;
1756
+ }
1757
+ /**
1758
+ * The response exceeded the byte cap. `declared` is what `Content-Length` claimed, present
1759
+ * only when the header was sent — and it counts *encoded* bytes where `bytesRead` counts
1760
+ * *decoded* bytes, so a `declared` far below `bytesRead` is evidence of a compression bomb
1761
+ * or a lying server rather than an arithmetic error.
1762
+ */
1763
+ | {
1764
+ readonly kind: 'too-large';
1765
+ readonly bytesRead: number;
1766
+ readonly limit: number;
1767
+ readonly declared?: number;
1768
+ }
1769
+ /** A response-headers guard rejected the response's content type. */
1770
+ | {
1771
+ readonly kind: 'unsupported-content-type';
1772
+ readonly contentType?: string;
1773
+ readonly accepted: ReadonlyArray<string>;
1774
+ }
1775
+ /** The response bytes could not be decoded to text with the indicated charset. */
1776
+ | {
1777
+ readonly kind: 'decode';
1778
+ readonly detail: string;
1779
+ }
1780
+ /** The decoded text could not be parsed, or failed the caller's converter. */
1781
+ | {
1782
+ readonly kind: 'parse';
1783
+ readonly detail: string;
1784
+ }
1785
+ /**
1786
+ * Anything else — including invalid options and a guard or transport that violated its
1787
+ * contract. Reports what it knows rather than guessing at a more specific kind.
1788
+ */
1789
+ | {
1790
+ readonly kind: 'unknown';
1791
+ readonly detail: string;
1792
+ };
1793
+
1794
+ /**
1795
+ * Which deadline a {@link SaferFetch.FetchFailureReason | timeout failure} belongs to.
1796
+ *
1797
+ * - `'headers'`: the per-attempt headers deadline elapsed before the server sent
1798
+ * response headers — the host is not answering.
1799
+ * - `'body'`: the overall deadline elapsed while the response body was being read
1800
+ * — the host answered and then trickled (the slowloris shape).
1801
+ * - `'overall'`: the overall deadline elapsed before response headers arrived.
1802
+ * @public
1803
+ */
1804
+ declare type FetchTimeoutPhase = 'headers' | 'body' | 'overall';
1805
+
1295
1806
  /**
1296
1807
  * Formats a list of items using the supplied template and formatter, one result
1297
1808
  * per output line.
@@ -1475,6 +1986,23 @@ declare function hexDecode(encoded: string): Result<Uint8Array>;
1475
1986
  */
1476
1987
  declare function hexEncode(data: Uint8Array): string;
1477
1988
 
1989
+ /**
1990
+ * Resolves a hostname to every address it names.
1991
+ *
1992
+ * @remarks
1993
+ * Injectable so that the resolving half of an address guard is testable without a network — a
1994
+ * guard test that performs a real lookup fails in CI for reasons unrelated to the guard, and a
1995
+ * guard test that could reach `169.254.169.254` is worse than flaky.
1996
+ *
1997
+ * An implementation must return a `Failure` rather than reject: `node:dns` rejects on
1998
+ * `ENOTFOUND`, `EAI_AGAIN` and friends, and an entry point documented to always return a
1999
+ * `Result` must not let that escape as a throw. {@link SaferFetch.nodeHostResolver} converts.
2000
+ *
2001
+ * @param hostname - The host to resolve, with no surrounding brackets.
2002
+ * @public
2003
+ */
2004
+ declare type HostResolver = (hostname: string) => Promise<Result<ReadonlyArray<string>>>;
2005
+
1478
2006
  /**
1479
2007
  * HPKE base mode (RFC 9180) — `DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM`.
1480
2008
  *
@@ -1678,6 +2206,85 @@ declare interface IAddKeyPairResult {
1678
2206
  readonly warning?: string;
1679
2207
  }
1680
2208
 
2209
+ /**
2210
+ * The evidence behind an allowed address-policy decision.
2211
+ * @public
2212
+ */
2213
+ declare interface IAddressCheckVerdict {
2214
+ /** The {@link IAddressPolicy.name | name} of the policy that produced the verdict. */
2215
+ readonly policy: string;
2216
+ /**
2217
+ * The classification of every address the policy examined, in the order
2218
+ * supplied. Empty for {@link allowAnyAddressPolicy}, which classifies nothing.
2219
+ */
2220
+ readonly addresses: ReadonlyArray<IClassifiedAddress>;
2221
+ }
2222
+
2223
+ /**
2224
+ * Decides whether a connection may be made. Invoked once per redirect hop, never only on
2225
+ * the initial URL.
2226
+ *
2227
+ * @remarks
2228
+ * This is the SSRF boundary and the one guard whose failure is catastrophic, which is why
2229
+ * it is a separate seam from the request and response policy guards: "did the address check
2230
+ * run, and run correctly?" should be answerable by reading one small implementation.
2231
+ *
2232
+ * The seam is **asynchronous and hop-chain-aware so that it can wrap a pure classifier**: a
2233
+ * real guard resolves `url.hostname` to an address list and then delegates the judgement to a
2234
+ * synchronous, address-list-in policy. That split keeps the part with the adversarial test
2235
+ * matrix — the address classification itself — free of transports, DNS, and hop bookkeeping,
2236
+ * and keeps exactly one implementation of it. A guard is the resolving, chain-aware half; it
2237
+ * should not re-derive what the classifier already decides.
2238
+ *
2239
+ * Classify `url.hostname`, never raw URL text: the WHATWG parser has already normalized
2240
+ * `127.0x.1` to `127.0.0.1` and `::ffff:169.254.169.254` to `::ffff:a9fe:a9fe`, and text
2241
+ * matching misses both.
2242
+ * @public
2243
+ */
2244
+ declare interface IAddressGuard {
2245
+ /** Stable identifier, surfaced in `'blocked-by-guard'` failures. */
2246
+ readonly name: string;
2247
+ /**
2248
+ * @param chain - Every hop so far, oldest first. The URL under consideration is the last
2249
+ * entry; `chain.length === 1` is the initial request. There is deliberately no separate
2250
+ * "is this a redirect" flag — hop 0 is not a special case, and a guard that compares only
2251
+ * against the immediately previous hop cannot see an `A → B → A` laundering hop.
2252
+ */
2253
+ check(chain: ReadonlyArray<IRequestHop>): Promise<Result<IGuardVerdict>>;
2254
+ }
2255
+
2256
+ /**
2257
+ * A pure, synchronous decision over a set of IP addresses.
2258
+ *
2259
+ * A policy performs no name resolution and no I/O: a caller resolves a
2260
+ * hostname to its addresses and hands the whole list to
2261
+ * {@link IAddressPolicy.checkAddresses}. The list contract is
2262
+ * **reject-if-any** — a hostname that resolves to one public and one private
2263
+ * address is disallowed, because without connect-time address pinning there is
2264
+ * no guarantee which address the connection will use.
2265
+ *
2266
+ * @public
2267
+ */
2268
+ declare interface IAddressPolicy {
2269
+ /**
2270
+ * A stable, greppable identifier for the posture this policy implements.
2271
+ * Surfaced in the failure message when an address is disallowed.
2272
+ */
2273
+ readonly name: string;
2274
+ /**
2275
+ * Decides whether a connection may be made to a destination that resolved to
2276
+ * `addresses`.
2277
+ *
2278
+ * @param addresses - every address the destination resolved to, or the
2279
+ * single literal address when the destination was an IP literal.
2280
+ * @returns `Success` with the {@link IAddressCheckVerdict | verdict} when
2281
+ * *every* address is permitted, `Failure` naming every address that is not.
2282
+ * An empty list is a failure for any policy that offers a guarantee: nothing
2283
+ * was verified, so nothing may be reached.
2284
+ */
2285
+ checkAddresses(addresses: ReadonlyArray<string>): Result<IAddressCheckVerdict>;
2286
+ }
2287
+
1681
2288
  /**
1682
2289
  * Options for adding an Argon2id password-derived secret.
1683
2290
  * @public
@@ -2745,6 +3352,71 @@ declare interface IArgon2idProvider {
2745
3352
  argon2id(password: Uint8Array | string, salt: Uint8Array, params: IArgon2idParams, options?: IArgon2idKeyingOptions): Promise<Result<Uint8Array>>;
2746
3353
  }
2747
3354
 
3355
+ /**
3356
+ * Options for {@link SaferFetch.blockPrivateNetworks}.
3357
+ * @public
3358
+ */
3359
+ declare interface IBlockPrivateNetworksGuardOptions extends IBlockPrivateNetworksOptions {
3360
+ /**
3361
+ * Restricts every hop to these hostnames. Absent means any hostname.
3362
+ *
3363
+ * @remarks
3364
+ * Matching is case-insensitive and exact — no wildcards and no suffix matching, because
3365
+ * `endsWith('.example.com')` is the classic host-allowlist bypass (`evil-example.com`,
3366
+ * `example.com.attacker.net`) and a primitive that offers the convenient form invites it.
3367
+ * List the hosts.
3368
+ *
3369
+ * **A host allowlist is the recommended posture**, and it is stronger than address
3370
+ * classification alone: with one, DNS rebinding can only be mounted by an allowlisted host's
3371
+ * own resolver, which is a far smaller surface than "any hostname the caller was handed".
3372
+ */
3373
+ readonly allowHosts?: ReadonlyArray<string>;
3374
+ /**
3375
+ * Restricts every hop to these ports. Absent means any port.
3376
+ *
3377
+ * @remarks
3378
+ * A URL with no explicit port is checked against the scheme's default — `443` for `https:`,
3379
+ * `80` for `http:` — so `allowPorts: [443]` accepts `https://example.com/`.
3380
+ */
3381
+ readonly allowPorts?: ReadonlyArray<number>;
3382
+ /**
3383
+ * Permits `http:` hops. **Off by default**, so this guard requires `https:`.
3384
+ *
3385
+ * @remarks
3386
+ * The core refuses everything that is not `http:` or `https:` and deliberately chooses
3387
+ * between those two here rather than there, because the choice is a posture rather than a
3388
+ * structural rule. Plaintext HTTP is exposed to a network-position attacker and is the scheme
3389
+ * every SSRF payload reaches for, so the polarity matches `allowLoopback`: deny by default,
3390
+ * opt in visibly, and let a reviewer grep for the opt-in.
3391
+ */
3392
+ readonly allowInsecureHttp?: boolean;
3393
+ /**
3394
+ * Name resolution. Defaults to {@link SaferFetch.nodeHostResolver}.
3395
+ *
3396
+ * @remarks
3397
+ * Present so the guard is unit-testable with no network. It is **not** a hook through which
3398
+ * DNS rebinding can be closed: swapping the resolver changes only the address the guard
3399
+ * validates, while the transport still connects by hostname and resolves again. Closing that
3400
+ * requires a pinning transport — see `IGuardVerdict.pinnedAddress`.
3401
+ */
3402
+ readonly resolve?: HostResolver;
3403
+ }
3404
+
3405
+ /**
3406
+ * Options shared by {@link blockPrivateNetworksPolicy} and the address guard built over it.
3407
+ * @public
3408
+ */
3409
+ declare interface IBlockPrivateNetworksOptions {
3410
+ /**
3411
+ * Permits `127.0.0.0/8` and `::1` (and their IPv4-mapped forms). Off by
3412
+ * default: the polarity that ships a guard permitting `http://127.0.0.1:6379/`
3413
+ * so a local-development convenience keeps working is the wrong one. A
3414
+ * caller talking to a local sidecar opts in here, and the opt-in is
3415
+ * independently greppable at the call site.
3416
+ */
3417
+ readonly allowLoopback?: boolean;
3418
+ }
3419
+
2748
3420
  /**
2749
3421
  * A single chat message in OpenAI format.
2750
3422
  * @public
@@ -2791,6 +3463,36 @@ declare interface IChatRequest {
2791
3463
  readonly messages: ReadonlyArray<IChatMessage>;
2792
3464
  }
2793
3465
 
3466
+ /**
3467
+ * The result of classifying a single IP address literal.
3468
+ * @public
3469
+ */
3470
+ declare interface IClassifiedAddress {
3471
+ /** The address exactly as supplied to {@link classifyAddress}. */
3472
+ readonly address: string;
3473
+ /**
3474
+ * A canonical textual form of the address: dotted-quad for IPv4, and
3475
+ * lowercase RFC 5952 hex-group form (with `::` run compression) for IPv6.
3476
+ * Note that IPv6 addresses embedding an IPv4 address canonicalize to their
3477
+ * hex-group form (`::ffff:a9fe:a9fe`), not to the dotted-quad mixed form;
3478
+ * `embeddedIpv4` carries the readable IPv4 value.
3479
+ */
3480
+ readonly canonical: string;
3481
+ /** The address family of the literal as supplied — never the embedded family. */
3482
+ readonly family: AddressFamily;
3483
+ /**
3484
+ * The classification used for policy decisions. For an IPv6 address that
3485
+ * embeds an IPv4 address, this is the classification of the *embedded* IPv4
3486
+ * address.
3487
+ */
3488
+ readonly classification: AddressClassification;
3489
+ /**
3490
+ * Present exactly when `classification` was derived from an IPv4 address
3491
+ * embedded in this IPv6 address, and absent otherwise.
3492
+ */
3493
+ readonly embeddedIpv4?: IEmbeddedIpv4;
3494
+ }
3495
+
2794
3496
  /**
2795
3497
  * Result of context validation, containing details about missing variables.
2796
3498
  * @public
@@ -3173,6 +3875,16 @@ declare interface ICryptoProvider {
3173
3875
  verifyHmacSha256(key: CryptoKey, signature: Uint8Array, data: Uint8Array): Promise<Result<boolean>>;
3174
3876
  }
3175
3877
 
3878
+ /**
3879
+ * Methods retried without an explicit opt-in.
3880
+ *
3881
+ * @remarks
3882
+ * A timeout does not tell you whether the server processed the request, so a retried `POST`
3883
+ * can double-charge. `retryNonIdempotent` opts out, per call, visibly.
3884
+ * @public
3885
+ */
3886
+ declare const IDEMPOTENT_METHODS: ReadonlyArray<string>;
3887
+
3176
3888
  /**
3177
3889
  * Parameters for creating a {@link DirectEncryptionProvider}.
3178
3890
  * @public
@@ -3194,6 +3906,17 @@ declare interface IDirectEncryptionProviderParams {
3194
3906
  readonly boundSecretName?: string;
3195
3907
  }
3196
3908
 
3909
+ /**
3910
+ * The IPv4 address an IPv6 address embedded, and the way it embedded it.
3911
+ * @public
3912
+ */
3913
+ declare interface IEmbeddedIpv4 {
3914
+ /** The encoding that carried the IPv4 address. */
3915
+ readonly kind: Ipv4EmbeddingKind;
3916
+ /** The dotted-quad form of the embedded IPv4 address. */
3917
+ readonly address: string;
3918
+ }
3919
+
3197
3920
  /**
3198
3921
  * Result of a raw-byte AES-256-GCM encryption via
3199
3922
  * {@link CryptoUtils.ICryptoProvider.encryptBytes | encryptBytes}. The
@@ -3512,6 +4235,34 @@ declare interface IFencedStringifiedJsonOptions<T> extends IFencedStringifiedJso
3512
4235
  readonly inner: Converter<T> | Validator<T>;
3513
4236
  }
3514
4237
 
4238
+ /**
4239
+ * Performs the actual request.
4240
+ *
4241
+ * @remarks
4242
+ * Injectable for two reasons. It is the seam through which a pinned-connect implementation
4243
+ * drops in without any caller-visible signature change — which is what keeps closing the
4244
+ * DNS-rebinding hole additive rather than breaking. And it is the test seam: redirect chains,
4245
+ * lying `Content-Length` headers, compression-bomb shapes, and trickling bodies are all
4246
+ * unit-testable through a scripted transport, with no live server anywhere in the suite.
4247
+ * @public
4248
+ */
4249
+ declare interface IFetchTransport {
4250
+ readonly name: string;
4251
+ fetch(url: URL, init: RequestInit, hints: IFetchTransportHints): Promise<Result<Response>>;
4252
+ }
4253
+
4254
+ /**
4255
+ * Out-of-band information a transport may need that `RequestInit` cannot express.
4256
+ * @public
4257
+ */
4258
+ declare interface IFetchTransportHints {
4259
+ /**
4260
+ * The address the address guard validated, to which this connection must be pinned.
4261
+ * A transport that cannot pin MUST fail rather than connect by hostname.
4262
+ */
4263
+ readonly pinnedAddress?: string;
4264
+ }
4265
+
3515
4266
  /**
3516
4267
  * Provider-specific config for Gemini Flash Image.
3517
4268
  * @public
@@ -3676,6 +4427,38 @@ declare interface IGrokImagineModelOptions extends INamedModelFamilyConfig {
3676
4427
  readonly config: IGrokImagineImageGenerationConfig;
3677
4428
  }
3678
4429
 
4430
+ /**
4431
+ * An address guard's decision about a hop.
4432
+ * @public
4433
+ */
4434
+ declare interface IGuardVerdict {
4435
+ /**
4436
+ * The URL cleared for request. Guards may **normalize** it — lowercase the host, strip a
4437
+ * trailing dot, punycode an IDN — and the normalized URL is what gets requested. Guards
4438
+ * must not **retarget** it to a different destination.
4439
+ *
4440
+ * @remarks
4441
+ * That contract is documented rather than enforced: origin-equality enforcement would
4442
+ * reject exactly the normalizations the guard is supposed to perform. Guards are trusted
4443
+ * first-party code; a malicious in-process caller is out of this primitive's threat model.
4444
+ * The one check that is enforced is the scheme — a verdict URL whose scheme is not
4445
+ * `http:` or `https:` is rejected.
4446
+ */
4447
+ readonly url: URL;
4448
+ /**
4449
+ * The address the guard validated and to which the connection SHOULD be pinned.
4450
+ *
4451
+ * @remarks
4452
+ * **Always `undefined` in this release** — reserved for the pinned-connect work that
4453
+ * closes the DNS-rebinding hole. A transport that receives a defined value it cannot
4454
+ * honor MUST fail rather than connect by hostname; {@link SaferFetch.platformFetchTransport}
4455
+ * does exactly that. Ignoring the hint would give a deployment that wired a pinning guard
4456
+ * but forgot the matching transport precisely the rebinding exposure it believed it had
4457
+ * closed, silently.
4458
+ */
4459
+ readonly pinnedAddress?: string;
4460
+ }
4461
+
3679
4462
  /**
3680
4463
  * Output of {@link HpkeProvider.sealBase}.
3681
4464
  *
@@ -4560,6 +5343,26 @@ declare interface IProviderListModelsParams {
4560
5343
  readonly endpoint?: string;
4561
5344
  }
4562
5345
 
5346
+ /**
5347
+ * The way an IPv6 address carried the IPv4 address whose classification was used.
5348
+ *
5349
+ * Each of these is a documented SSRF bypass: an address such as
5350
+ * `::ffff:169.254.169.254` is an IPv6 address by family, but the address the
5351
+ * connection actually reaches is the embedded IPv4 one, so the embedded
5352
+ * address is what must be classified.
5353
+ *
5354
+ * @public
5355
+ */
5356
+ declare type Ipv4EmbeddingKind =
5357
+ /** `::ffff:0:0/96` — the IPv4-mapped IPv6 form. */
5358
+ 'ipv4-mapped'
5359
+ /** `::/96` — the deprecated (RFC 4291) IPv4-compatible form. */
5360
+ | 'ipv4-compatible'
5361
+ /** `64:ff9b::/96` — the RFC 6052 well-known NAT64 prefix. */
5362
+ | 'nat64'
5363
+ /** `2002::/16` — the RFC 3056 6to4 form, whose IPv4 lives in bits 16..47. */
5364
+ | '6to4';
5365
+
4563
5366
  /**
4564
5367
  * Result of removing a secret from the key store.
4565
5368
  * @public
@@ -4578,6 +5381,46 @@ declare interface IRemoveSecretResult {
4578
5381
  readonly warning?: string;
4579
5382
  }
4580
5383
 
5384
+ /**
5385
+ * Policy guard over the outbound request.
5386
+ *
5387
+ * @remarks
5388
+ * **Reject-only.** Silently rewriting a caller's headers or body inside a fetch primitive
5389
+ * means the caller no longer knows what was sent. To alter the request, return an explicit
5390
+ * replacement the caller could have observed.
5391
+ *
5392
+ * Runs **before** the address guard, so an address guard is always the last word before the
5393
+ * connect: a replacement request that changed the URL is still address-checked.
5394
+ * @public
5395
+ */
5396
+ declare interface IRequestGuard {
5397
+ readonly name: string;
5398
+ check(request: ISaferFetchRequest, chain: ReadonlyArray<IRequestHop>): Promise<Result<ISaferFetchRequest>>;
5399
+ }
5400
+
5401
+ /**
5402
+ * One hop in a redirect chain. Entry 0 is the caller's original request.
5403
+ * @public
5404
+ */
5405
+ declare interface IRequestHop {
5406
+ /** The URL under consideration for this hop. */
5407
+ readonly url: URL;
5408
+ /** The redirect status that produced the NEXT hop; absent on the current one. */
5409
+ readonly status?: number;
5410
+ /**
5411
+ * The address actually connected to on this hop, when address pinning was in effect.
5412
+ *
5413
+ * @remarks
5414
+ * Populated from the address guard's {@link SaferFetch.IGuardVerdict.pinnedAddress} once the
5415
+ * transport has accepted it — which is sound because a transport that cannot honor a pin is
5416
+ * required to fail rather than connect by hostname, so a completed request with a pin set is
5417
+ * evidence the pin held. **Undefined throughout this release**, since no shipped guard pins
5418
+ * and {@link SaferFetch.platformFetchTransport} cannot honor one; the field is where the
5419
+ * rebinding defense's per-hop evidence will live.
5420
+ */
5421
+ readonly connectedAddress?: string;
5422
+ }
5423
+
4581
5424
  /**
4582
5425
  * Required version of options with all fields populated.
4583
5426
  * @internal
@@ -4589,6 +5432,19 @@ declare interface IRequiredMustacheTemplateOptions {
4589
5432
  readonly escape: MustacheEscapeStrategy;
4590
5433
  }
4591
5434
 
5435
+ /**
5436
+ * The guards a call will actually use. Every field is concrete: defaults are applied once, at
5437
+ * the boundary, and nothing downstream branches on a guard's absence.
5438
+ * @public
5439
+ */
5440
+ declare interface IResolvedGuards {
5441
+ /** Caller-supplied; there is no default. */
5442
+ readonly address: IAddressGuard;
5443
+ readonly request: IRequestGuard;
5444
+ readonly responseHeaders: IResponseHeadersGuard;
5445
+ readonly responseBody: IResponseBodyGuard;
5446
+ }
5447
+
4592
5448
  /**
4593
5449
  * The resolved, merged wire parameters for an image generation request.
4594
5450
  * Built from the layered options and ready for provider-specific encoding.
@@ -4635,6 +5491,106 @@ declare interface IResolvedThinkingConfig {
4635
5491
  readonly otherParams?: JsonObject;
4636
5492
  }
4637
5493
 
5494
+ /**
5495
+ * Policy guard over the buffered response body.
5496
+ *
5497
+ * @remarks
5498
+ * Runs on the buffered body, after the size cap has been enforced — so it can never be handed
5499
+ * an unbounded stream.
5500
+ * @public
5501
+ */
5502
+ declare interface IResponseBodyGuard {
5503
+ readonly name: string;
5504
+ check(body: Uint8Array, head: ISaferFetchResponseHead): Promise<Result<true>>;
5505
+ }
5506
+
5507
+ /**
5508
+ * Policy guard over the response head.
5509
+ *
5510
+ * @remarks
5511
+ * Runs before any body bytes are read, so a rejection costs a header comparison instead of a
5512
+ * partial body transfer. This is the right layer for content-type gating — see
5513
+ * {@link SaferFetch.allowContentTypes}.
5514
+ * @public
5515
+ */
5516
+ declare interface IResponseHeadersGuard {
5517
+ readonly name: string;
5518
+ /**
5519
+ * The media types this guard accepts, when it is a content-type allowlist.
5520
+ *
5521
+ * @remarks
5522
+ * Present so that a rejection can be reported as `'unsupported-content-type'` carrying the
5523
+ * accepted list, rather than as an opaque policy rejection the caller has to parse a message
5524
+ * to understand. A guard that is not a content-type allowlist omits it and its rejections are
5525
+ * reported as `'blocked-by-guard'`.
5526
+ */
5527
+ readonly acceptedContentTypes?: ReadonlyArray<string>;
5528
+ check(head: ISaferFetchResponseHead, chain: ReadonlyArray<IRequestHop>): Promise<Result<true>>;
5529
+ }
5530
+
5531
+ /**
5532
+ * Opt-in retry policy.
5533
+ *
5534
+ * @remarks
5535
+ * **Retry is off by default and has to be asked for.** A primitive that silently retries
5536
+ * changes the semantics of every call site and amplifies load against a service that is already
5537
+ * struggling.
5538
+ *
5539
+ * Two rules are load-bearing rather than incidental, and are worth reading before enabling this:
5540
+ *
5541
+ * **Every attempt re-runs the address guard from hop 0 — a full re-walk, never a resume and
5542
+ * never a cached verdict.** Reusing an earlier attempt's verdict would make retry its own
5543
+ * DNS-rebinding vector: an attacker needs the guard to pass once and a connect to land on a
5544
+ * private address, so `N` connects against **one** check multiplies their odds by the retry
5545
+ * count — and a retry delay is precisely when a short-TTL rebind lands. A resume is
5546
+ * independently wrong because the redirect chain is not stable across attempts: attempt 2's
5547
+ * server may answer with a different `Location`, so revalidating only the current hop would
5548
+ * validate a chain the request no longer follows. The consequence is that retries cost more
5549
+ * than a naive implementation would suggest — each carries full re-resolution — and that cost
5550
+ * is deliberate.
5551
+ *
5552
+ * **The overall deadline is the ceiling.** Retries consume `timeoutMs` and never reset it, so
5553
+ * `{ timeoutMs: 30_000, retry: { attempts: 3 } }` is a thirty-second call, not a two-minute
5554
+ * one. When the remaining budget is shorter than the computed delay, the call fails with the
5555
+ * attempt's own failure rather than sleeping past its deadline.
5556
+ * @public
5557
+ */
5558
+ declare interface IRetryPolicy {
5559
+ /**
5560
+ * Additional attempts after the first, **not** a total. `attempts: 2` means up to three
5561
+ * requests. Must be a non-negative integer; `0` disables retry.
5562
+ */
5563
+ readonly attempts: number;
5564
+ /**
5565
+ * First backoff delay, in milliseconds, doubled per attempt before jitter. Default
5566
+ * {@link SaferFetch.DEFAULT_RETRY_BASE_DELAY_MS}.
5567
+ */
5568
+ readonly baseDelayMs?: number;
5569
+ /**
5570
+ * Ceiling on any single delay, in milliseconds. Default
5571
+ * {@link SaferFetch.DEFAULT_RETRY_MAX_DELAY_MS}. Also clamps a server-supplied
5572
+ * `Retry-After`, which is the security-relevant half of its job.
5573
+ */
5574
+ readonly maxDelayMs?: number;
5575
+ /**
5576
+ * Retries `POST` / `PUT` / `PATCH` / `DELETE` as well as `GET` / `HEAD`. Default `false`.
5577
+ *
5578
+ * @remarks
5579
+ * Off by default because a timeout does not tell you whether the server processed the
5580
+ * request: a retried `POST` after a timeout can double-charge. Turn it on for endpoints you
5581
+ * know are idempotent — an idempotency key, a `PUT` of a whole resource — and not otherwise.
5582
+ */
5583
+ readonly retryNonIdempotent?: boolean;
5584
+ /**
5585
+ * Honors a `Retry-After` header on `429` and `503`. Default `true`.
5586
+ *
5587
+ * @remarks
5588
+ * Always clamped to `maxDelayMs`, whatever this is set to. The header is chosen by the
5589
+ * server, which in this threat model may be the adversary.
5590
+ */
5591
+ readonly respectRetryAfter?: boolean;
5592
+ }
5593
+
4638
5594
  /**
4639
5595
  * Determines whether a concrete (already-resolved) Anthropic model id uses the adaptive
4640
5596
  * thinking wire shape (`thinking: { type: 'adaptive' }` + top-level `output_config.effort`)
@@ -4661,6 +5617,182 @@ declare interface IResolvedThinkingConfig {
4661
5617
  */
4662
5618
  declare function isAdaptiveThinkingModel(descriptor: IAiProviderDescriptor, modelId: string): boolean;
4663
5619
 
5620
+ /**
5621
+ * Options for the validating form of {@link SaferFetch.saferFetchJson}.
5622
+ *
5623
+ * @remarks
5624
+ * `converter` is **required** here, and `T` is inferred from it. There is deliberately no way
5625
+ * to assert a `T` without supplying the converter that evidences it at runtime: a caller-named
5626
+ * type with nothing checking it is a claim the primitive cannot keep. Omit the whole options
5627
+ * type and the value comes back as `JsonValue`, which is what the wire actually guarantees.
5628
+ * @public
5629
+ */
5630
+ declare interface ISaferFetchJsonOptions<T> extends ISaferFetchOptions {
5631
+ /** Applied to the parsed JSON, taking the caller from wire to validated `T` in one step. */
5632
+ readonly converter: Converter<T> | Validator<T>;
5633
+ }
5634
+
5635
+ /**
5636
+ * Options common to every safer-fetch entry point.
5637
+ * @public
5638
+ */
5639
+ declare interface ISaferFetchOptions {
5640
+ /**
5641
+ * **Required, with no default.** A passthrough default here would be exactly the failure
5642
+ * this primitive exists to avoid — advertising a guarantee it does not have. Requiring it
5643
+ * makes omission a *compile* error rather than a runtime surprise or a lint rule, and makes
5644
+ * every call site's posture greppable in one search with no ambient default to overlook.
5645
+ *
5646
+ * Use a named factory. `allowAnyAddress()` is the explicit, deliberately uncomfortable
5647
+ * opt-out, and is the only correct choice in a browser, where neither DNS resolution nor
5648
+ * redirect interposition exists.
5649
+ */
5650
+ readonly addressGuard: IAddressGuard;
5651
+ /**
5652
+ * Policy guard over the outbound request. Optional here and non-optional once resolved —
5653
+ * it defaults to silent passthrough, applied once at the boundary, so no downstream code
5654
+ * path branches on a guard's absence.
5655
+ */
5656
+ readonly requestGuard?: IRequestGuard;
5657
+ /**
5658
+ * Policy guard over the response head. Defaults to silent passthrough. Content-type gating
5659
+ * lives here — see {@link SaferFetch.allowContentTypes}; it is deliberately not a separate
5660
+ * option, which would be a second mechanism for one job.
5661
+ */
5662
+ readonly responseHeadersGuard?: IResponseHeadersGuard;
5663
+ /** Policy guard over the buffered body. Defaults to silent passthrough. */
5664
+ readonly responseBodyGuard?: IResponseBodyGuard;
5665
+ /** Defaults to `'GET'`. */
5666
+ readonly method?: SaferFetchMethod;
5667
+ /** Request headers. Names are lowercased before the request guard sees them. */
5668
+ readonly headers?: Readonly<Record<string, string>>;
5669
+ /** Request body. Not permitted with `'GET'` or `'HEAD'`. */
5670
+ readonly body?: string | Uint8Array;
5671
+ /**
5672
+ * Overall deadline in milliseconds, covering the connect, the response headers, and the
5673
+ * body read. Default {@link SaferFetch.DEFAULT_TIMEOUT_MS}. This is the deadline that stops
5674
+ * a response dribbling one byte every 25 seconds, which passes every per-read check.
5675
+ */
5676
+ readonly timeoutMs?: number;
5677
+ /**
5678
+ * Deadline to first response headers, in milliseconds. Default
5679
+ * {@link SaferFetch.DEFAULT_HEADERS_TIMEOUT_MS}. Distinguishes "the host is not answering"
5680
+ * from "the host is answering slowly", which the failure taxonomy then reports as
5681
+ * `timeout.phase`.
5682
+ *
5683
+ * @remarks
5684
+ * Measured from the start of the attempt, which includes guard evaluation — an address guard
5685
+ * that resolves DNS spends this budget too. That is deliberate: the deadline bounds the time
5686
+ * a caller waits for a usable response, not the time one layer of the implementation spends.
5687
+ */
5688
+ readonly headersTimeoutMs?: number;
5689
+ /**
5690
+ * Cap on decoded response bytes. Default {@link SaferFetch.DEFAULT_MAX_RESPONSE_BYTES}
5691
+ * (5 MiB).
5692
+ *
5693
+ * @remarks
5694
+ * **This knob is meant to be reached for.** 5 MiB is roughly an order of magnitude above a
5695
+ * realistic JSON API response and is generous for text, but real documents clear it — raise
5696
+ * it per call for the calls that need it rather than treating the default as a ceiling. The
5697
+ * cap is enforced by counting decoded bytes during the read, so raising it raises the
5698
+ * buffer the process may be asked to hold; size it against the heap you are willing to
5699
+ * spend, not against the largest document you can imagine.
5700
+ */
5701
+ readonly maxResponseBytes?: number;
5702
+ /**
5703
+ * How redirects are handled. Defaults to `'reject'`.
5704
+ *
5705
+ * @remarks
5706
+ * The conservative default is deliberate, and is the same polarity as `addressGuard` having no
5707
+ * default: one core serves both runtimes, `'reject'` is the only mode whose guarantee is
5708
+ * identical on each, and following a redirect chain into hosts the caller never named is a
5709
+ * posture worth spelling at the call site. Callers ingesting real-world URLs want
5710
+ * `'validate-each-hop'`.
5711
+ */
5712
+ readonly redirectPolicy?: SaferFetchRedirectPolicy;
5713
+ /**
5714
+ * Cap on redirect hops followed under `'validate-each-hop'`. Default
5715
+ * {@link SaferFetch.DEFAULT_MAX_REDIRECTS} (5). Must be a non-negative integer; `0` refuses
5716
+ * to follow any redirect.
5717
+ */
5718
+ readonly maxRedirects?: number;
5719
+ /**
5720
+ * Additional header names to drop on a cross-origin redirect hop, on top of the always-dropped
5721
+ * `authorization`, `cookie` and `proxy-authorization`
5722
+ * ({@link SaferFetch.ALWAYS_STRIPPED_HEADERS}).
5723
+ *
5724
+ * @remarks
5725
+ * Matching is case-insensitive. Use this for bearer-equivalent headers a deployment invented —
5726
+ * `x-api-key`, `x-auth-token`, a signed-request header. Stripping is **monotonic**: once a hop
5727
+ * has left an origin the headers are gone for the rest of the chain, so an `A` → `B` → `A`
5728
+ * chain does not hand the credential back to `A` after `B` has watched it leave.
5729
+ */
5730
+ readonly sensitiveHeaders?: ReadonlyArray<string>;
5731
+ /**
5732
+ * Opt-in retry. **Off by default** — a primitive that silently retries changes the semantics
5733
+ * of every call site and amplifies load against a service that is already struggling.
5734
+ *
5735
+ * @remarks
5736
+ * Retries consume `timeoutMs` and never reset it, so enabling retry does not extend the
5737
+ * deadline; and every attempt re-runs the address guard from hop 0 as a full re-walk, never a
5738
+ * resume and never a cached verdict. Both rules are load-bearing rather than incidental — see
5739
+ * {@link SaferFetch.IRetryPolicy}, which states why.
5740
+ */
5741
+ readonly retry?: IRetryPolicy;
5742
+ /** Defaults to {@link SaferFetch.platformFetchTransport}. */
5743
+ readonly transport?: IFetchTransport;
5744
+ /** Caller's cancellation signal. Reported as `'aborted'`, never as `'timeout'`. */
5745
+ readonly signal?: AbortSignal;
5746
+ /** Diagnostics sink. Defaults to a no-op logger. */
5747
+ readonly logger?: Logging.ILogger;
5748
+ }
5749
+
5750
+ /**
5751
+ * The request a safer-fetch call is about to issue.
5752
+ * @public
5753
+ */
5754
+ declare interface ISaferFetchRequest {
5755
+ readonly url: URL;
5756
+ readonly method: SaferFetchMethod;
5757
+ /** Header names are lowercased. */
5758
+ readonly headers: Readonly<Record<string, string>>;
5759
+ readonly body?: string | Uint8Array;
5760
+ }
5761
+
5762
+ /**
5763
+ * A successful safer-fetch response.
5764
+ * @public
5765
+ */
5766
+ declare interface ISaferFetchResponse<T> {
5767
+ readonly value: T;
5768
+ readonly status: number;
5769
+ /** Header names are lowercased. */
5770
+ readonly headers: Readonly<Record<string, string>>;
5771
+ /**
5772
+ * Every URL actually requested, in order, as cleared by the address guard. `[0]` is the
5773
+ * caller's; later entries are redirect hops. A caller that allowlisted `api.example.com`
5774
+ * and was redirected to `cdn.example.com` usually wants to know.
5775
+ */
5776
+ readonly urlChain: ReadonlyArray<string>;
5777
+ /** Decoded bytes read from the response body. */
5778
+ readonly bytesRead: number;
5779
+ }
5780
+
5781
+ /**
5782
+ * Response status and headers, available before any body bytes are read.
5783
+ * @public
5784
+ */
5785
+ declare interface ISaferFetchResponseHead {
5786
+ readonly status: number;
5787
+ readonly statusText: string;
5788
+ /** Header names are lowercased. */
5789
+ readonly headers: Readonly<Record<string, string>>;
5790
+ /** Raw `content-type` header value, including any parameters. */
5791
+ readonly contentType?: string;
5792
+ /** Parsed `content-length`, present only when the header was sent and is a valid count. */
5793
+ readonly contentLength?: number;
5794
+ }
5795
+
4664
5796
  /**
4665
5797
  * Checks if a JSON object appears to be an encrypted file.
4666
5798
  * Uses the format field as a discriminator.
@@ -4923,7 +6055,20 @@ declare interface IYamlSerializeOptions {
4923
6055
  }
4924
6056
 
4925
6057
  /**
4926
- * Simple interface for a file to be added to a zip file.
6058
+ * Interface for a file to be added to a zip file, whose contents are either text or
6059
+ * raw bytes.
6060
+ *
6061
+ * @remarks
6062
+ * String contents are encoded as UTF-8; `Uint8Array` contents are stored verbatim.
6063
+ * @public
6064
+ */
6065
+ declare interface IZipFile {
6066
+ readonly path: string;
6067
+ readonly contents: string | Uint8Array;
6068
+ }
6069
+
6070
+ /**
6071
+ * Simple interface for a text file to be added to a zip file.
4927
6072
  * @public
4928
6073
  */
4929
6074
  declare interface IZipTextFile {
@@ -4951,6 +6096,57 @@ declare interface JarRecordParserOptions {
4951
6096
  readonly fixedContinuationSize?: number;
4952
6097
  }
4953
6098
 
6099
+ /**
6100
+ * Typed reason a JSON-shaped LLM response failed to parse, so a caller can
6101
+ * branch on the failure class instead of regex-matching the engine's
6102
+ * `JSON.parse` message (whose wording varies across V8 / Node versions).
6103
+ *
6104
+ * Every classified arm describes a fault at an **object property-name
6105
+ * position** — the position an LLM most often gets wrong, and the one whose
6106
+ * repair strategy differs most by case:
6107
+ *
6108
+ * - `'unquoted-property-name'`: a bare identifier where a quoted name belongs
6109
+ * (`{ key: 1 }`). `token` is the identifier run, `offset` its first
6110
+ * character. Identifier recognition is ASCII-only, so a non-ASCII bare name
6111
+ * (`{ ключ: 1 }`) reports `'unknown'` rather than being named.
6112
+ * - `'single-quoted-property-name'`: a single-quoted name (`{ 'key': 1 }`).
6113
+ * `token` is the quoted literal (or just `'` if it never closes), `offset`
6114
+ * the opening quote.
6115
+ * - `'unterminated-property-name'`: a name whose closing `"` is missing, and
6116
+ * whose body swallowed structural text (`{ "key: 1 }`). `token` is the
6117
+ * unterminated fragment, `offset` the opening quote.
6118
+ * - `'elided-member'`: a `,` where a member is expected — a leading or doubled
6119
+ * comma in an object (`{ , "a": 1 }`, `{ "a":1, , "b":2 }`) or an array
6120
+ * (`[1, , 2]`). `token` is `','`, `offset` its position.
6121
+ * - `'unknown'`: the catch-all. The scan reached the end of the text, or hit a
6122
+ * fault it cannot name with confidence, and reports nothing rather than
6123
+ * guessing. Truncated responses, missing colons, trailing commas, bad number
6124
+ * literals, and anything else not listed above land here.
6125
+ *
6126
+ * `offset` is a 0-based index into the `text` passed to
6127
+ * {@link AiAssist.classifyJsonParseFailure}, not into the extracted substring.
6128
+ * @public
6129
+ */
6130
+ declare type JsonParseFailureReason = {
6131
+ readonly kind: 'unquoted-property-name';
6132
+ readonly token: string;
6133
+ readonly offset: number;
6134
+ } | {
6135
+ readonly kind: 'single-quoted-property-name';
6136
+ readonly token: string;
6137
+ readonly offset: number;
6138
+ } | {
6139
+ readonly kind: 'unterminated-property-name';
6140
+ readonly token: string;
6141
+ readonly offset: number;
6142
+ } | {
6143
+ readonly kind: 'elided-member';
6144
+ readonly token: string;
6145
+ readonly offset: number;
6146
+ } | {
6147
+ readonly kind: 'unknown';
6148
+ };
6149
+
4954
6150
  /**
4955
6151
  * Controls the optional system-prompt augmentation applied by
4956
6152
  * {@link AiAssist.generateJsonCompletion}.
@@ -5817,6 +7013,35 @@ declare const modelSpec: Converter<ModelSpec>;
5817
7013
 
5818
7014
  /**
5819
7015
  * Known context keys for model specification maps.
7016
+ *
7017
+ * @remarks
7018
+ * Two axes live here and nothing else: the **quality tier** (`base` / `advanced`
7019
+ * / `frontier`) selects the *completion* model, and `image` / `embedding` select
7020
+ * the non-completion modalities.
7021
+ *
7022
+ * There is deliberately **no `tools` or `thinking` key**. Both existed before the
7023
+ * quality-tier axis landed and were removed with it: server-side tools and
7024
+ * reasoning effort are orthogonal *request* params that ride on top of whatever
7025
+ * model the tier already selected — they never select a model. A tool-using or
7026
+ * thinking-enabled call passes a tier like any other call (omit → `base`, or
7027
+ * `'advanced'` / `'frontier'`) and sets the tools / thinking request params
7028
+ * independently. Thinking composes with any tier without a tier-level capability
7029
+ * check — but that is a statement about the tier axis, not a claim that every
7030
+ * provider supports thinking: several descriptors declare
7031
+ * `thinkingMode: 'unsupported'` (e.g. `copy-paste`, `groq`, `mistral`, `ollama`,
7032
+ * `openai-compat`).
7033
+ *
7034
+ * Thinking availability is declared **per provider**, on the descriptor's
7035
+ * `thinkingMode`; the descriptor does not encode per-model thinking availability at
7036
+ * all, so a provider that declares support may still have individual models its own
7037
+ * API rejects thinking on. (`adaptiveThinkingModelPrefixes` is per-model but selects
7038
+ * a wire *shape*, not availability.) What the tier axis guarantees is therefore
7039
+ * narrow and exact: a tier selects a model within one provider and never changes the
7040
+ * provider, so it never changes `thinkingMode`.
7041
+ *
7042
+ * Do not add a `'tools'` or `'thinking'` key here, and do not hand-roll a
7043
+ * `resolveModel` + `resolveModelAlias` walk to emulate one — call
7044
+ * `resolveProviderModel` with the tier you want.
5820
7045
  * @public
5821
7046
  */
5822
7047
  declare type ModelSpecKey = 'base' | 'advanced' | 'frontier' | 'image' | 'embedding';
@@ -6209,6 +7434,24 @@ declare class NodeCryptoProvider implements ICryptoProvider {
6209
7434
  */
6210
7435
  declare const nodeCryptoProvider: NodeCryptoProvider;
6211
7436
 
7437
+ /**
7438
+ * The default {@link SaferFetch.HostResolver}: `node:dns`'s `lookup`, returning every address.
7439
+ *
7440
+ * @remarks
7441
+ * `lookup` rather than `resolve4`/`resolve6` deliberately. `lookup` goes through the operating
7442
+ * system's resolver — the same path `fetch`'s connect takes — so it sees `/etc/hosts`, `nsswitch`
7443
+ * ordering, and any local override. `resolve4` queries DNS directly and would miss a
7444
+ * `hosts`-file entry pointing an allowlisted name at `127.0.0.1`, which is a bypass rather than
7445
+ * a curiosity.
7446
+ *
7447
+ * `all: true` because the list contract is reject-if-any: a name resolving to one public and one
7448
+ * private address must be refused, and asking for one address would hide the second.
7449
+ *
7450
+ * A rejected lookup (`ENOTFOUND`, `EAI_AGAIN`, …) becomes a `Failure`, never a throw.
7451
+ * @public
7452
+ */
7453
+ declare const nodeHostResolver: HostResolver;
7454
+
6212
7455
  /**
6213
7456
  * Model IDs for OpenAI thinking-capable models.
6214
7457
  * @public
@@ -6240,6 +7483,23 @@ declare function parseRecordJarLines(lines: string[], options?: JarRecordParserO
6240
7483
  */
6241
7484
  declare const pbkdf2KeyDerivationParams: Converter<IPbkdf2KeyDerivationParams>;
6242
7485
 
7486
+ /**
7487
+ * The {@link SaferFetch.IFetchTransport | transport} that wraps `globalThis.fetch`.
7488
+ *
7489
+ * @remarks
7490
+ * **Fails loudly when asked to honor a `pinnedAddress`.** `globalThis.fetch` connects by
7491
+ * hostname and re-resolves DNS itself, so it cannot honor a pin. Ignoring the hint instead
7492
+ * would mean that a deployment which wired an address-pinning guard but forgot the matching
7493
+ * pinning transport gets exactly the DNS-rebinding exposure it believed it had closed, and
7494
+ * gets it silently. A primitive that advertises a guarantee it does not have is worse than no
7495
+ * primitive — and that applies to future versions of itself.
7496
+ *
7497
+ * No guard in this release populates `pinnedAddress`, so the interlock is dormant. It is here
7498
+ * so that populating it later is an additive change that cannot be made unsafely.
7499
+ * @public
7500
+ */
7501
+ declare const platformFetchTransport: IFetchTransport;
7502
+
6243
7503
  /**
6244
7504
  * Returns the canonical `CryptoUtils.KeyStore.KeyStore` secret name for a provider's API key,
6245
7505
  * of the form `provider:<providerId>`. Apps that store provider API keys in a `KeyStore`
@@ -6438,6 +7698,13 @@ declare namespace RecordJar {
6438
7698
  }
6439
7699
  export { RecordJar }
6440
7700
 
7701
+ /**
7702
+ * Redirect statuses this primitive recognizes as redirects. Other 3xx statuses (`300`, `304`,
7703
+ * …) do not direct the client at a new URL and are reported as ordinary non-2xx responses.
7704
+ * @public
7705
+ */
7706
+ declare const REDIRECT_STATUSES: ReadonlyArray<number>;
7707
+
6441
7708
  /**
6442
7709
  * Resolves the effective tools for a completion call.
6443
7710
  *
@@ -6460,10 +7727,20 @@ declare function resolveEffectiveTools(descriptor: IAiProviderDescriptor, settin
6460
7727
  * `modelPrefix` is the longest prefix of `modelId`. Ties are broken by
6461
7728
  * first-encountered.
6462
7729
  *
7730
+ * @remarks
7731
+ * `modelId` may be either a concrete provider model id or an fgv model alias
7732
+ * (`@<provider>:<role>`, see `MODEL_ALIAS_SIGIL`) — it is resolved via
7733
+ * `resolveModelAlias` against `descriptor.aliases` before prefix matching,
7734
+ * so both forms select the same capability. A raw provider id passes through
7735
+ * unchanged. An alias that is not registered on `descriptor` (or is cyclic)
7736
+ * names no model and yields `undefined` rather than falling through to the
7737
+ * `modelPrefix: ''` catch-all.
7738
+ *
6463
7739
  * @param descriptor - The provider descriptor
6464
- * @param modelId - The resolved embedding model id
6465
- * @returns The matching capability, or `undefined` when no rule matches or the
6466
- * provider declares no embedding capabilities.
7740
+ * @param modelId - The embedding model id — concrete or an fgv alias
7741
+ * @returns The matching capability, or `undefined` when no rule matches, the
7742
+ * provider declares no embedding capabilities, or `modelId` is an
7743
+ * unresolvable alias.
6467
7744
  * @public
6468
7745
  */
6469
7746
  declare function resolveEmbeddingCapability(descriptor: IAiProviderDescriptor, modelId: string): IAiEmbeddingModelCapability | undefined;
@@ -6476,10 +7753,20 @@ declare function resolveEmbeddingCapability(descriptor: IAiProviderDescriptor, m
6476
7753
  * order does not matter for correctness — only for tie-breaking among rules
6477
7754
  * with identical-length prefixes (an unusual case).
6478
7755
  *
7756
+ * @remarks
7757
+ * `modelId` may be either a concrete provider model id or an fgv model alias
7758
+ * (`@<provider>:<role>`, see `MODEL_ALIAS_SIGIL`) — it is resolved via
7759
+ * `resolveModelAlias` against `descriptor.aliases` before prefix matching,
7760
+ * so both forms select the same capability. A raw provider id passes through
7761
+ * unchanged. An alias that is not registered on `descriptor` (or is cyclic)
7762
+ * names no model and yields `undefined` rather than falling through to the
7763
+ * `modelPrefix: ''` catch-all.
7764
+ *
6479
7765
  * @param descriptor - The provider descriptor
6480
- * @param modelId - The resolved image model id
6481
- * @returns The matching capability, or `undefined` when no rule matches or
6482
- * the provider declares no image-generation capabilities.
7766
+ * @param modelId - The image model id — concrete or an fgv alias
7767
+ * @returns The matching capability, or `undefined` when no rule matches, the
7768
+ * provider declares no image-generation capabilities, or `modelId` is an
7769
+ * unresolvable alias.
6483
7770
  * @public
6484
7771
  */
6485
7772
  declare function resolveImageCapability(descriptor: IAiProviderDescriptor, modelId: string): IAiImageModelCapability | undefined;
@@ -6560,6 +7847,20 @@ declare function resolveModelAlias(descriptor: IAiProviderDescriptor, model: str
6560
7847
  * `ModelSpec` branch is selected first; the resulting string — which may itself
6561
7848
  * be an fgv alias — is then resolved to a concrete id.
6562
7849
  *
7850
+ * **This is the whole model-selection surface — do not hand-roll the walk.**
7851
+ * Callers should pass the `ModelSpecKey` they want and use the concrete id
7852
+ * this returns; a manual `resolveModel` + `resolveModelAlias` sequence is
7853
+ * both redundant and easy to get wrong (it is how alias-form ids leak into
7854
+ * capability lookups such as `resolveImageCapability`).
7855
+ *
7856
+ * **`context` carries the quality tier and the modality — never tools or
7857
+ * thinking.** `ModelSpecKey` has no `tools` / `thinking` key: server-side
7858
+ * tools and reasoning effort are orthogonal request params that ride on top of
7859
+ * whatever model the tier selected, and never select a model. A tool-path caller
7860
+ * passes a tier like any other caller — omit `context` for `base`, or pass
7861
+ * `'advanced'` / `'frontier'` — and sets the tools / thinking request params
7862
+ * separately.
7863
+ *
6563
7864
  * @param descriptor - The provider descriptor (supplies `defaultModel` and `aliases`).
6564
7865
  * @param modelOverride - An optional caller-supplied `ModelSpec` that takes precedence
6565
7866
  * over `descriptor.defaultModel`. May itself contain or be an alias.
@@ -6569,6 +7870,220 @@ declare function resolveModelAlias(descriptor: IAiProviderDescriptor, model: str
6569
7870
  */
6570
7871
  declare function resolveProviderModel(descriptor: IAiProviderDescriptor, modelOverride: ModelSpec | undefined, context?: ModelSpecKey): Result<string>;
6571
7872
 
7873
+ /**
7874
+ * Statuses whose `Retry-After` header is honored.
7875
+ *
7876
+ * @remarks
7877
+ * `429` and `503` are the two the header is specified for and the two where it carries real
7878
+ * scheduling information. Honoring it on a `500` would let any failing endpoint dictate the
7879
+ * caller's schedule for no benefit.
7880
+ * @public
7881
+ */
7882
+ declare const RETRY_AFTER_STATUSES: ReadonlyArray<number>;
7883
+
7884
+ /**
7885
+ * HTTP statuses a {@link SaferFetch.IRetryPolicy | retry policy} will retry.
7886
+ *
7887
+ * @remarks
7888
+ * Every other status — including every other 4xx — is terminal. A `401`, a `403` and a `404`
7889
+ * mean the same thing on the second attempt as on the first, and retrying them turns a client
7890
+ * bug into load against a service that already said no.
7891
+ * @public
7892
+ */
7893
+ declare const RETRYABLE_HTTP_STATUSES: ReadonlyArray<number>;
7894
+
7895
+ declare namespace SaferFetch {
7896
+ export {
7897
+ ALWAYS_STRIPPED_HEADERS,
7898
+ DEFAULT_HEADERS_TIMEOUT_MS,
7899
+ DEFAULT_MAX_REDIRECTS,
7900
+ DEFAULT_MAX_RESPONSE_BYTES,
7901
+ DEFAULT_RETRY_BASE_DELAY_MS,
7902
+ DEFAULT_RETRY_MAX_DELAY_MS,
7903
+ DEFAULT_TIMEOUT_MS,
7904
+ IDEMPOTENT_METHODS,
7905
+ REDIRECT_STATUSES,
7906
+ RETRY_AFTER_STATUSES,
7907
+ RETRYABLE_HTTP_STATUSES,
7908
+ SUPPORTED_SCHEMES,
7909
+ FetchFailureReason,
7910
+ FetchTimeoutPhase,
7911
+ IRetryPolicy,
7912
+ IAddressGuard,
7913
+ IFetchTransport,
7914
+ IFetchTransportHints,
7915
+ IGuardVerdict,
7916
+ IRequestGuard,
7917
+ IRequestHop,
7918
+ IResolvedGuards,
7919
+ IResponseBodyGuard,
7920
+ IResponseHeadersGuard,
7921
+ ISaferFetchOptions,
7922
+ ISaferFetchRequest,
7923
+ ISaferFetchResponse,
7924
+ ISaferFetchResponseHead,
7925
+ SaferFetchMethod,
7926
+ SaferFetchRedirectPolicy,
7927
+ allowAnyAddress,
7928
+ allowContentTypes,
7929
+ platformFetchTransport,
7930
+ saferFetchBytes,
7931
+ saferFetchJson,
7932
+ saferFetchText,
7933
+ ISaferFetchJsonOptions,
7934
+ allowAnyAddressPolicy,
7935
+ blockPrivateNetworksPolicy,
7936
+ IAddressCheckVerdict,
7937
+ IAddressPolicy,
7938
+ IBlockPrivateNetworksOptions,
7939
+ blockPrivateNetworks,
7940
+ nodeHostResolver,
7941
+ HostResolver,
7942
+ IBlockPrivateNetworksGuardOptions,
7943
+ classifyAddress,
7944
+ AddressClassification,
7945
+ AddressFamily,
7946
+ IClassifiedAddress,
7947
+ IEmbeddedIpv4,
7948
+ Ipv4EmbeddingKind
7949
+ }
7950
+ }
7951
+ export { SaferFetch }
7952
+
7953
+ /**
7954
+ * Fetches a URL and returns the raw response bytes, subject to the scheme check, the address
7955
+ * guard, the request and response guards, the deadlines, and the response size cap.
7956
+ *
7957
+ * @remarks
7958
+ * **What this does not protect against.** It is not a sandbox: a caller who wants to reach
7959
+ * `127.0.0.1` can call `globalThis.fetch` directly, so this guards untrusted *data*, never
7960
+ * untrusted *code in the same process*. It does no certificate pinning and adds nothing
7961
+ * against a network-position attacker — TLS is that control. It does not resist DNS rebinding:
7962
+ * an address guard validates a resolved address and the transport then re-resolves, so a
7963
+ * hostile resolver can answer the two lookups differently. And it inspects nothing in the
7964
+ * response body. If your deployment has an egress proxy or firewall, that control is strictly
7965
+ * stronger than this one; this is defense in depth for deployments that do not.
7966
+ *
7967
+ * **The failure detail is an internal-network scanning oracle.** Log it; do not echo it, or
7968
+ * any string derived from it, to an untrusted caller. The detail is structured precisely so
7969
+ * that mapping it to a coarse public code is trivial.
7970
+ *
7971
+ * **Redirects are rejected unless you ask for them.** `redirectPolicy: 'validate-each-hop'`
7972
+ * follows them, runs the address guard on every hop before any connection, and drops credential
7973
+ * headers the first time the chain leaves an origin — and never restores them, so an
7974
+ * `A` → `B` → `A` chain does not hand the token back to `A`.
7975
+ *
7976
+ * @param url - The URL to fetch. Only `http:` and `https:` are ever requested; every other
7977
+ * scheme fails as `'invalid-url'`.
7978
+ * @param options - Call options. `addressGuard` is required and has no default — use
7979
+ * `blockPrivateNetworks()` on Node, or `allowAnyAddress()` to name the absence of the
7980
+ * guarantee at the call site. `maxResponseBytes` defaults to 5 MiB and is meant to be tuned
7981
+ * per call: real documents clear that, and raising it for the calls that need it is the
7982
+ * intended use, not a workaround.
7983
+ * @public
7984
+ */
7985
+ declare function saferFetchBytes(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<Uint8Array>, FetchFailureReason>>;
7986
+
7987
+ /**
7988
+ * Fetches a URL and parses the response body as JSON, yielding the raw `JsonValue` for the
7989
+ * caller to validate.
7990
+ *
7991
+ * @remarks
7992
+ * **This does not gate on `Content-Type` by itself.** A server returning an HTML error page
7993
+ * with a `200` will be decoded and parsed, and fail as `'parse'` — twenty frames from where it
7994
+ * actually went wrong. Pass `responseHeadersGuard: allowContentTypes(['application/json'])`
7995
+ * to reject on the header instead: it costs a header comparison rather than a body transfer,
7996
+ * and the failure names the type. Content-type gating is a guard rather than an option so that
7997
+ * there is exactly one mechanism for it.
7998
+ *
7999
+ * See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
8000
+ * and for the warning about echoing failure detail to untrusted callers.
8001
+ *
8002
+ * @param url - The URL to fetch.
8003
+ * @param options - Call options. `addressGuard` is required. `maxResponseBytes` defaults to
8004
+ * 5 MiB and is meant to be tuned per call.
8005
+ * @public
8006
+ */
8007
+ declare function saferFetchJson(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<JsonValue>, FetchFailureReason>>;
8008
+
8009
+ /**
8010
+ * Fetches a URL, parses the response body as JSON, and runs it through the supplied converter
8011
+ * or validator so the caller reaches a validated `T` in one step.
8012
+ *
8013
+ * @remarks
8014
+ * `T` is inferred from `converter` and is never caller-asserted, so the returned type is
8015
+ * evidenced at runtime rather than claimed.
8016
+ *
8017
+ * See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
8018
+ * and for the warning about echoing failure detail to untrusted callers.
8019
+ *
8020
+ * @param url - The URL to fetch.
8021
+ * @param options - Call options plus the required `converter`. `addressGuard` is required.
8022
+ * `maxResponseBytes` defaults to 5 MiB and is meant to be tuned per call.
8023
+ * @public
8024
+ */
8025
+ declare function saferFetchJson<T>(url: string | URL, options: ISaferFetchJsonOptions<T>): Promise<DetailedResult<ISaferFetchResponse<T>, FetchFailureReason>>;
8026
+
8027
+ /**
8028
+ * HTTP methods a safer-fetch call may use.
8029
+ * @public
8030
+ */
8031
+ declare type SaferFetchMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
8032
+
8033
+ /**
8034
+ * How redirects are handled.
8035
+ *
8036
+ * @remarks
8037
+ * `'reject'` fails on any redirect status. It is the default, and the only mode with an
8038
+ * equivalent guarantee on every runtime — a browser cannot inspect a redirect hop at all, so a
8039
+ * per-hop revalidating mode is not implementable there.
8040
+ *
8041
+ * The *guarantee* is equivalent on both runtimes; the failure **reason** is not. Every call uses
8042
+ * `redirect: 'manual'`, so on Node a rejected redirect surfaces as `'redirect-rejected'` carrying
8043
+ * the status, while in a browser the response is opaque — `type` is `'opaqueredirect'` and
8044
+ * `status` is `0`, so there is no status to report — and the same redirect surfaces as
8045
+ * `'redirect-opaque'`. Callers that branch on the reason under `'reject'` must handle both.
8046
+ *
8047
+ * `'validate-each-hop'` follows redirects with `redirect: 'manual'` and runs the **full address
8048
+ * guard on every hop before any connection is made**, resolving `Location` against the hop that
8049
+ * sent it. Redirect handling and the address check are one mechanism, not two: a guard that
8050
+ * validated only the caller's URL is defeated by a single `302` to `http://169.254.169.254/`.
8051
+ * Following hops also makes this primitive responsible for credential stripping — see
8052
+ * `ISaferFetchOptions.sensitiveHeaders`.
8053
+ *
8054
+ * **`'validate-each-hop'` is accepted by the browser barrel but cannot succeed there, and fails
8055
+ * loudly rather than degrading.** The type is shared because one core serves both runtimes; the
8056
+ * runtime is not. A browser's `redirect: 'manual'` yields an opaque response — `type` is
8057
+ * `'opaqueredirect'`, `status` is `0`, and `Location` is not readable — so the first redirect
8058
+ * fails as `'redirect-opaque'`. That is the honest outcome: the hop information does not exist
8059
+ * on the browser side of the API, so there is nothing to guard and nothing to follow. Use
8060
+ * `'reject'` there, or handle `'redirect-opaque'`.
8061
+ *
8062
+ * A mode that defers to the platform's own redirect following is deliberately absent on Node:
8063
+ * it would put hops on the wire that the guard never saw.
8064
+ * @public
8065
+ */
8066
+ declare type SaferFetchRedirectPolicy = 'reject' | 'validate-each-hop';
8067
+
8068
+ /**
8069
+ * Fetches a URL and decodes the response body as text.
8070
+ *
8071
+ * @remarks
8072
+ * The charset comes from the `Content-Type` parameter and defaults to UTF-8. Decoding is
8073
+ * strict: an unknown charset, or a byte sequence that is not valid in the declared one, fails
8074
+ * as `'decode'` rather than silently producing mojibake that some caller downstream treats as
8075
+ * data.
8076
+ *
8077
+ * See {@link SaferFetch.saferFetchBytes} for what this primitive does **not** protect against,
8078
+ * and for the warning about echoing failure detail to untrusted callers.
8079
+ *
8080
+ * @param url - The URL to fetch.
8081
+ * @param options - Call options. `addressGuard` is required. `maxResponseBytes` defaults to
8082
+ * 5 MiB and is meant to be tuned per call.
8083
+ * @public
8084
+ */
8085
+ declare function saferFetchText(url: string | URL, options: ISaferFetchOptions): Promise<DetailedResult<ISaferFetchResponse<string>, FetchFailureReason>>;
8086
+
6572
8087
  /**
6573
8088
  * Function type for dynamic secret retrieval.
6574
8089
  * @public
@@ -6621,6 +8136,18 @@ declare const SMART_JSON_PROMPT_HINT: string;
6621
8136
  */
6622
8137
  declare function spkiToRawX25519(spki: Uint8Array): Result<Uint8Array>;
6623
8138
 
8139
+ /**
8140
+ * URL schemes this primitive will ever request.
8141
+ *
8142
+ * @remarks
8143
+ * Core rejects everything else — `file:`, `data:`, `blob:`, `ftp:`, `gopher:`, `ws:` — outright
8144
+ * and at every hop, because none of them is a legitimate network fetch and each is a standard
8145
+ * SSRF payload. Deciding between `http:` and `https:`, and which ports are acceptable, belongs
8146
+ * to the address guard, which is why core does not narrow this further.
8147
+ * @public
8148
+ */
8149
+ declare const SUPPORTED_SCHEMES: ReadonlyArray<string>;
8150
+
6624
8151
  /**
6625
8152
  * Whether a provider declares any embedding capability at all.
6626
8153
  *
@@ -6802,9 +8329,15 @@ declare class ZipDirectoryItem<TCT extends string = string> implements FileTree.
6802
8329
  * Implementation of `FileTree.IFileTreeFileItem` for files in a ZIP archive.
6803
8330
  * ZIP files are read-only, so this item does not support mutation.
6804
8331
  * Use {@link FileTree.isMutableFileItem | isMutableFileItem} to check before attempting mutations.
8332
+ *
8333
+ * @remarks
8334
+ * ZIP entries are byte-native, so this item also implements the read half of the
8335
+ * optional binary capability (`FileTree.IBinaryFileTreeFileItem`) — use
8336
+ * `FileTree.isBinaryFileItem` to narrow and `getRawBytes()` to read the entry's
8337
+ * undecoded bytes.
6805
8338
  * @public
6806
8339
  */
6807
- declare class ZipFileItem<TCT extends string = string> implements FileTree.IFileTreeFileItem<TCT> {
8340
+ declare class ZipFileItem<TCT extends string = string> implements FileTree.IBinaryFileTreeFileItem<TCT> {
6808
8341
  /**
6809
8342
  * Indicates that this `FileTree.FileTreeItem` is a file.
6810
8343
  */
@@ -6830,9 +8363,13 @@ declare class ZipFileItem<TCT extends string = string> implements FileTree.IFile
6830
8363
  */
6831
8364
  get contentType(): TCT | undefined;
6832
8365
  /**
6833
- * The pre-loaded contents of the file.
8366
+ * The pre-loaded raw bytes of the ZIP entry.
8367
+ */
8368
+ private readonly _bytes;
8369
+ /**
8370
+ * Text form of the entry, decoded lazily from the raw bytes on first text read.
6834
8371
  */
6835
- private readonly _contents;
8372
+ private _contents;
6836
8373
  /**
6837
8374
  * The ZIP file tree accessors that created this item.
6838
8375
  */
@@ -6848,10 +8385,12 @@ declare class ZipFileItem<TCT extends string = string> implements FileTree.IFile
6848
8385
  /**
6849
8386
  * Constructor for ZipFileItem.
6850
8387
  * @param zipFilePath - The path of the file within the ZIP.
6851
- * @param contents - The pre-loaded contents of the file.
8388
+ * @param contents - The pre-loaded contents of the entry, either as raw bytes or as
8389
+ * already-decoded text. Text is encoded as UTF-8 for the byte accessor and is also
8390
+ * retained verbatim, so supplying text never round-trips through a decode.
6852
8391
  * @param accessors - The ZIP file tree accessors.
6853
8392
  */
6854
- constructor(zipFilePath: string, contents: string, accessors: ZipFileTreeAccessors<TCT>);
8393
+ constructor(zipFilePath: string, contents: string | Uint8Array, accessors: ZipFileTreeAccessors<TCT>);
6855
8394
  /**
6856
8395
  * Sets the content type of the file.
6857
8396
  * @param contentType - The content type of the file.
@@ -6864,8 +8403,21 @@ declare class ZipFileItem<TCT extends string = string> implements FileTree.IFile
6864
8403
  getContents<T>(converter: Validator<T> | Converter<T>): Result<T>;
6865
8404
  /**
6866
8405
  * Gets the raw contents of the file as a string.
8406
+ *
8407
+ * @remarks
8408
+ * The entry's bytes are decoded as UTF-8 with the lenient WHATWG default, so
8409
+ * malformed input is silently replaced with U+FFFD. Use `getRawBytes()` for the
8410
+ * undecoded bytes — and `new TextDecoder('utf-8', { fatal: true }).decode(bytes)`
8411
+ * for a decode that fails loudly instead.
6867
8412
  */
6868
8413
  getRawContents(): Result<string>;
8414
+ /**
8415
+ * Gets the raw bytes of the ZIP entry, with no text decoding applied.
8416
+ *
8417
+ * @remarks
8418
+ * The returned array is the item's internal buffer and must not be modified.
8419
+ */
8420
+ getRawBytes(): Result<Uint8Array>;
6869
8421
  }
6870
8422
 
6871
8423
  declare namespace ZipFileTree {
@@ -6874,7 +8426,9 @@ declare namespace ZipFileTree {
6874
8426
  ZipFileItem,
6875
8427
  ZipDirectoryItem,
6876
8428
  createZipFromTextFiles,
8429
+ createZipFromFiles,
6877
8430
  IZipTextFile,
8431
+ IZipFile,
6878
8432
  ZipCompressionLevel,
6879
8433
  ICreateZipOptions
6880
8434
  }
@@ -6885,9 +8439,15 @@ export { ZipFileTree }
6885
8439
  * Read-only file tree accessors for ZIP archives.
6886
8440
  * ZIP archives are read-only by design — use {@link FileTree.isMutableAccessors | isMutableAccessors}
6887
8441
  * to check before attempting mutations.
8442
+ *
8443
+ * @remarks
8444
+ * ZIP entries are byte-native, so these accessors also implement the read half of the
8445
+ * optional binary capability (`FileTree.IBinaryFileTreeAccessors`) — use
8446
+ * `FileTree.isBinaryAccessors` to narrow and `getFileBytes()` to read an entry's
8447
+ * undecoded bytes.
6888
8448
  * @public
6889
8449
  */
6890
- declare class ZipFileTreeAccessors<TCT extends string = string> implements FileTree.IFileTreeAccessors<TCT> {
8450
+ declare class ZipFileTreeAccessors<TCT extends string = string> implements FileTree.IBinaryFileTreeAccessors<TCT> {
6891
8451
  /**
6892
8452
  * The unzipped file data.
6893
8453
  */
@@ -6981,8 +8541,25 @@ declare class ZipFileTreeAccessors<TCT extends string = string> implements FileT
6981
8541
  getItem(path: string): Result<FileTree.FileTreeItem<TCT>>;
6982
8542
  /**
6983
8543
  * Gets the contents of a file in the file tree.
8544
+ *
8545
+ * @remarks
8546
+ * The entry's bytes are decoded as UTF-8 with the lenient WHATWG default. Use
8547
+ * `getFileBytes()` for the undecoded bytes.
6984
8548
  */
6985
8549
  getFileContents(path: string): Result<string>;
8550
+ /**
8551
+ * Gets the raw bytes of a ZIP entry, with no text decoding applied.
8552
+ *
8553
+ * @remarks
8554
+ * The returned array is the archive's internal buffer and must not be modified.
8555
+ */
8556
+ getFileBytes(path: string): Result<Uint8Array>;
8557
+ /**
8558
+ * Resolves a path to the ZIP file item it names.
8559
+ * @param path - Path of the entry to look up.
8560
+ * @returns `Success` with the item, or `Failure` if it is missing or is a directory.
8561
+ */
8562
+ private _getFileItem;
6986
8563
  /**
6987
8564
  * Gets the content type of a file in the file tree.
6988
8565
  */