@fedify/vocab-runtime 2.5.0-dev.2446 → 2.5.0-dev.2492

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 (121) hide show
  1. package/deno.json +5 -2
  2. package/dist/{docloader-BBnBpbsG.d.cts → docloader-DD6binpN.d.cts} +4 -8
  3. package/dist/{docloader-BBnBpbsG.d.ts → docloader-DD6binpN.d.ts} +4 -8
  4. package/dist/internal/jsonld-cache.cjs +1 -1
  5. package/dist/internal/jsonld-cache.d.cts +1 -1
  6. package/dist/internal/jsonld-cache.d.ts +1 -1
  7. package/dist/internal/jsonld-cache.js +1 -1
  8. package/dist/internal/portable-dereference.cjs +72 -7
  9. package/dist/internal/portable-dereference.d.cts +63 -3
  10. package/dist/internal/portable-dereference.d.ts +63 -3
  11. package/dist/internal/portable-dereference.js +72 -8
  12. package/dist/internal.cjs +6 -0
  13. package/dist/internal.d.cts +108 -0
  14. package/dist/internal.d.ts +108 -0
  15. package/dist/internal.js +3 -0
  16. package/dist/mod.cjs +104 -38
  17. package/dist/mod.d.cts +3 -336
  18. package/dist/mod.d.ts +4 -336
  19. package/dist/mod.js +104 -38
  20. package/dist/{portable-BDydjp5H.d.ts → portable-1VSnSJFj.d.ts} +1 -1
  21. package/dist/{portable-Dfvcg_Wy.d.cts → portable-Cr7NwZwb.d.cts} +1 -1
  22. package/dist/public-fetch-Dvfvawei.cjs +527 -0
  23. package/dist/public-fetch-ziZ39L5I.js +510 -0
  24. package/dist/tests/{body-Ddx6WgR5.cjs → body-C8d1PmDa.cjs} +1 -1
  25. package/dist/tests/{body-CQi2bVi5.mjs → body-HSLxhexN.mjs} +1 -1
  26. package/dist/tests/body.test.cjs +1 -1
  27. package/dist/tests/body.test.mjs +1 -1
  28. package/dist/tests/decimal.test.cjs +4 -4
  29. package/dist/tests/decimal.test.mjs +4 -4
  30. package/dist/tests/{docloader-C3vDXwn1.cjs → docloader-D1fNCGam.cjs} +111 -39
  31. package/dist/tests/{docloader-CrVannnU.mjs → docloader-DkEUFJui.mjs} +106 -40
  32. package/dist/tests/docloader.test.cjs +531 -1293
  33. package/dist/tests/docloader.test.mjs +472 -1252
  34. package/dist/tests/esm-Ds-eHTfY.mjs +1229 -0
  35. package/dist/tests/esm-Q2tdh5f3.cjs +1215 -0
  36. package/dist/tests/internal/portable-dereference.test.cjs +3 -3
  37. package/dist/tests/internal/portable-dereference.test.mjs +3 -3
  38. package/dist/tests/{jsonld-cache-BEXiiM-V.cjs → jsonld-cache-DKIWKxEX.cjs} +1 -1
  39. package/dist/tests/{jsonld-cache-DFnWsLuZ.mjs → jsonld-cache-KH40z9_7.mjs} +1 -1
  40. package/dist/tests/jsonld-cache.test.cjs +2 -2
  41. package/dist/tests/jsonld-cache.test.mjs +2 -2
  42. package/dist/tests/node-CbBR-th-.mjs +84 -0
  43. package/dist/tests/node-Ch29PDXR.cjs +89 -0
  44. package/dist/tests/pin-grant-CPrpk1l4.cjs +70 -0
  45. package/dist/tests/pin-grant-H5XhM1Xs.mjs +53 -0
  46. package/dist/tests/pin-grant.test.cjs +87 -0
  47. package/dist/tests/pin-grant.test.d.cts +1 -0
  48. package/dist/tests/pin-grant.test.d.mts +1 -0
  49. package/dist/tests/pin-grant.test.mjs +88 -0
  50. package/dist/tests/pinners/bun.test.cjs +166 -0
  51. package/dist/tests/pinners/bun.test.d.cts +1 -0
  52. package/dist/tests/pinners/bun.test.d.mts +1 -0
  53. package/dist/tests/pinners/bun.test.mjs +167 -0
  54. package/dist/tests/pinners/node.test.cjs +269 -0
  55. package/dist/tests/pinners/node.test.d.cts +1 -0
  56. package/dist/tests/pinners/node.test.d.mts +1 -0
  57. package/dist/tests/pinners/node.test.mjs +268 -0
  58. package/dist/tests/pinners/pinners.test.cjs +196 -0
  59. package/dist/tests/pinners/pinners.test.d.cts +1 -0
  60. package/dist/tests/pinners/pinners.test.d.mts +1 -0
  61. package/dist/tests/pinners/pinners.test.mjs +197 -0
  62. package/dist/tests/pinners/testdata/node-child.cjs +135 -0
  63. package/dist/tests/pinners/testdata/node-child.d.cts +1 -0
  64. package/dist/tests/pinners/testdata/node-child.d.mts +1 -0
  65. package/dist/tests/pinners/testdata/node-child.mjs +134 -0
  66. package/dist/tests/pinners-DLH_npLf.cjs +220 -0
  67. package/dist/tests/pinners-tGcl49uF.mjs +203 -0
  68. package/dist/tests/{portable-dereference-DaTBBvCj.cjs → portable-dereference-BUoDzMxk.cjs} +2 -2
  69. package/dist/tests/{portable-dereference-sGRs_qdT.mjs → portable-dereference-Du2k3ngE.mjs} +2 -2
  70. package/dist/tests/{portable-media-BJf_gGQz.cjs → portable-media-BM9zgj9g.cjs} +3 -3
  71. package/dist/tests/{portable-media-Cc58dUeE.mjs → portable-media-CTxGibcj.mjs} +3 -3
  72. package/dist/tests/portable-media.test.cjs +1 -1
  73. package/dist/tests/portable-media.test.mjs +1 -1
  74. package/dist/tests/portable-workers.test.cjs +2 -2
  75. package/dist/tests/portable-workers.test.mjs +2 -2
  76. package/dist/tests/public-fetch-BGj5pzUD.cjs +186 -0
  77. package/dist/tests/public-fetch-Cxo3Yzdf.mjs +170 -0
  78. package/dist/tests/public-fetch.test.cjs +1073 -0
  79. package/dist/tests/public-fetch.test.d.cts +1 -0
  80. package/dist/tests/public-fetch.test.d.mts +1 -0
  81. package/dist/tests/public-fetch.test.mjs +1074 -0
  82. package/dist/tests/{request-glvxfx2U.cjs → request-7q-Zfe1p.cjs} +2 -2
  83. package/dist/tests/{request-1pPyYz3e.mjs → request-D5uGtmmq.mjs} +2 -2
  84. package/dist/tests/request.test.cjs +2 -2
  85. package/dist/tests/request.test.mjs +2 -2
  86. package/dist/tests/support-Bj44xlQu.mjs +272 -0
  87. package/dist/tests/support-BrdlbLJk.cjs +363 -0
  88. package/dist/tests/{url-BNakuZ8k.cjs → url-Bdj-rTcC.cjs} +88 -12
  89. package/dist/tests/{url-DMxmp7ZG.mjs → url-COj5Q8mb.mjs} +65 -13
  90. package/dist/tests/url.test.cjs +4 -1
  91. package/dist/tests/url.test.mjs +4 -1
  92. package/dist/url-4EE56sNP.d.cts +352 -0
  93. package/dist/url-4EE56sNP.d.ts +352 -0
  94. package/dist/{url-DrGTR8yv.cjs → url-DIKZnaJv.cjs} +88 -12
  95. package/dist/{url-Dzyp-NsC.js → url-mWveR5GA.js} +65 -13
  96. package/package.json +14 -3
  97. package/src/docloader.test.ts +696 -48
  98. package/src/docloader.ts +186 -75
  99. package/src/internal/portable-dereference.ts +116 -6
  100. package/src/internal.ts +13 -0
  101. package/src/pin-grant.test.ts +118 -0
  102. package/src/pin-grant.ts +99 -0
  103. package/src/pinners/bun.test.ts +278 -0
  104. package/src/pinners/bun.ts +223 -0
  105. package/src/pinners/mod.ts +59 -0
  106. package/src/pinners/node.test.ts +415 -0
  107. package/src/pinners/node.ts +144 -0
  108. package/src/pinners/pinners.test.ts +316 -0
  109. package/src/pinners/testdata/node-child.ts +147 -0
  110. package/src/pinners/testdata/proxy-child.ts +82 -0
  111. package/src/pinners/testdata/support.ts +343 -0
  112. package/src/pinners/testdata/tls-cert.pem +12 -0
  113. package/src/pinners/testdata/tls-child.ts +88 -0
  114. package/src/pinners/testdata/tls-key.pem +5 -0
  115. package/src/pinners/types.ts +48 -0
  116. package/src/public-fetch.test.ts +1286 -0
  117. package/src/public-fetch.ts +338 -0
  118. package/src/testdata/public-fetch-deno-worker.ts +74 -0
  119. package/src/url.test.ts +11 -0
  120. package/src/url.ts +113 -22
  121. package/tsdown.config.ts +7 -2
@@ -0,0 +1,527 @@
1
+
2
+ const require_url = require("./url-DIKZnaJv.cjs");
3
+ let _logtape_logtape = require("@logtape/logtape");
4
+ let node_net = require("node:net");
5
+ //#region src/pin-grant.ts
6
+ const grants = /* @__PURE__ */ new WeakMap();
7
+ /**
8
+ * Issues a grant for the given URL's origin and addresses. This does not
9
+ * validate the addresses; callers validate them before issuing, and pinners
10
+ * validate them again right before connecting.
11
+ * @param url The URL whose origin the grant is bound to.
12
+ * @param addresses The addresses to connect to.
13
+ * @returns A new grant.
14
+ */
15
+ function issuePinGrant(url, addresses) {
16
+ const grant = Object.freeze(Object.create(null));
17
+ let hostname = url.hostname;
18
+ if (hostname.startsWith("[") && hostname.endsWith("]")) hostname = hostname.slice(1, -1);
19
+ const record = Object.freeze({
20
+ origin: url.origin,
21
+ hostname,
22
+ addresses: Object.freeze(addresses.map(({ address, family }) => Object.freeze({
23
+ address,
24
+ family
25
+ })))
26
+ });
27
+ grants.set(grant, {
28
+ record,
29
+ revoked: false
30
+ });
31
+ return grant;
32
+ }
33
+ /**
34
+ * Checks that the given grant was issued by {@link issuePinGrant}, has not
35
+ * been revoked, and was issued for the origin of the given URL.
36
+ * @param grant The grant to redeem.
37
+ * @param url The URL to connect to.
38
+ * @returns What the grant stands for.
39
+ * @throws {TypeError} If the grant cannot be used for the URL.
40
+ */
41
+ function redeemPinGrant(grant, url) {
42
+ const state = typeof grant === "object" && grant !== null ? grants.get(grant) : void 0;
43
+ if (state == null) throw new TypeError("Invalid pin grant.");
44
+ if (state.revoked) throw new TypeError("The pin grant has been revoked.");
45
+ if (url.origin !== state.record.origin) throw new TypeError(`The pin grant for ${state.record.origin} cannot be used for ${url.origin}.`);
46
+ return state.record;
47
+ }
48
+ /**
49
+ * Revokes the given grant so that it can no longer be redeemed.
50
+ * @param grant The grant to revoke.
51
+ */
52
+ function revokePinGrant(grant) {
53
+ const state = grants.get(grant);
54
+ if (state != null) state.revoked = true;
55
+ }
56
+ //#endregion
57
+ //#region src/pinners/bun.ts
58
+ const logger$1 = (0, _logtape_logtape.getLogger)([
59
+ "fedify",
60
+ "runtime",
61
+ "fetch"
62
+ ]);
63
+ /**
64
+ * The first Bun version that honors `proxy: false` when proxy environment
65
+ * variables are set at startup. Earlier versions send pinned requests
66
+ * through the proxy, with the validated IP address as the target.
67
+ */
68
+ const PROXY_FALSE_MIN_VERSION = "1.4.3";
69
+ const PROXY_VARIABLES = [
70
+ "HTTP_PROXY",
71
+ "http_proxy",
72
+ "HTTPS_PROXY",
73
+ "https_proxy",
74
+ "ALL_PROXY",
75
+ "all_proxy"
76
+ ];
77
+ const FALLBACK_ERROR_CODES = /* @__PURE__ */ new Set([
78
+ "ConnectionRefused",
79
+ "ECONNREFUSED",
80
+ "EHOSTUNREACH",
81
+ "ENETUNREACH",
82
+ "ETIMEDOUT"
83
+ ]);
84
+ /**
85
+ * Bun versions before 1.3.11 verify the server certificate against the `Host`
86
+ * header, including a non-default port, instead of `tls.serverName`, so
87
+ * pinned requests to HTTPS servers on non-default ports always fail the check
88
+ * there. A custom `checkServerIdentity` cannot stand in for it: these
89
+ * versions send the whole request before a failing custom check aborts it
90
+ * (and 1.3.10 even reuses the connection for the next request), so such
91
+ * requests are refused instead.
92
+ */
93
+ const HOST_HEADER_IDENTITY_RANGE = "<1.3.11";
94
+ /**
95
+ * Creates a pinner for Bun. Every request is sent to an IP literal URL with
96
+ * the original `Host` header, `tls.serverName` set to the original hostname
97
+ * (so that the certificate is still verified against it), and `proxy: false`.
98
+ *
99
+ * GET and HEAD requests fall back to the next address on transport errors
100
+ * raised before a response arrives. Other methods use only the first
101
+ * address, since a transport error does not guarantee that the request did
102
+ * not reach the server, and retrying it could deliver an activity twice.
103
+ * Together with the transport retries of `doubleKnock()`, a GET request is
104
+ * attempted at most twice per address. Note that Bun 1.4 also reports
105
+ * a connection reset after the request was sent as a `TypeError`, so GET and
106
+ * HEAD requests may be repeated against the next address in that case, which
107
+ * is acceptable for these idempotent methods.
108
+ *
109
+ * Bun versions before 1.3.11 verify certificates against the `Host` header,
110
+ * which includes a non-default port, and cannot safely check them against
111
+ * the original hostname in any other way, so requests to HTTPS servers on
112
+ * non-default ports fail there with a `TypeError` before connecting.
113
+ *
114
+ * Bun versions before 1.4.3 ignore `proxy: false` when proxy environment
115
+ * variables are set, so requests go through the proxy with the validated IP
116
+ * address as the target. In that case a warning is logged once.
117
+ * @param options The pinner options.
118
+ * @returns The pinner.
119
+ */
120
+ function createBunPinner({ verifyAddress }) {
121
+ let proxyWarningPending = ignoresProxyFalse() && hasProxyVariables();
122
+ return {
123
+ supports(url) {
124
+ return url.protocol === "http:" || url.protocol === "https:";
125
+ },
126
+ async fetch(grant, request, signal) {
127
+ const url = new URL(request.url);
128
+ const { hostname, addresses } = redeemPinGrant(grant, url);
129
+ if (proxyWarningPending) {
130
+ proxyWarningPending = false;
131
+ logger$1.warn("Bun {version} ignores proxy: false while proxy environment variables are set, so outbound requests go through the proxy instead of connecting directly to the validated addresses. Upgrade to Bun {minVersion} or later, or unset the proxy environment variables; see {url}.", {
132
+ version: getBun()?.version,
133
+ minVersion: PROXY_FALSE_MIN_VERSION,
134
+ url: "https://fedify.dev/manual/deploy#bun-behind-an-http-proxy"
135
+ });
136
+ }
137
+ const fallback = request.method === "GET" || request.method === "HEAD";
138
+ const candidates = fallback ? addresses : addresses.slice(0, 1);
139
+ const body = request.body == null ? null : await request.arrayBuffer();
140
+ let lastError = /* @__PURE__ */ new TypeError(`No address to connect to for ${url.host}.`);
141
+ for (const address of candidates) {
142
+ throwIfAborted(signal, request.signal);
143
+ verifyAddress(address);
144
+ const headers = new Headers(request.headers);
145
+ headers.set("Host", url.host);
146
+ const init = {
147
+ method: request.method,
148
+ headers,
149
+ body,
150
+ redirect: "manual",
151
+ signal: signal == null || signal === request.signal ? request.signal : anySignal(signal, request.signal),
152
+ proxy: false
153
+ };
154
+ if (url.protocol === "https:") init.tls = getTlsOptions(url, hostname);
155
+ try {
156
+ return await fetch(toAddressUrl(url, address), init);
157
+ } catch (error) {
158
+ throwIfAborted(signal, request.signal);
159
+ if (!fallback || !isFallbackError(error)) throw error;
160
+ lastError = error;
161
+ }
162
+ }
163
+ throw lastError;
164
+ },
165
+ release() {}
166
+ };
167
+ }
168
+ function getTlsOptions(url, hostname) {
169
+ const bun = getBun();
170
+ if (url.port !== "" && bun != null && bun.semver.satisfies(bun.version, HOST_HEADER_IDENTITY_RANGE)) throw new TypeError(`Bun ${bun.version} cannot verify the certificate of ${url.host} when connecting to a validated address; upgrade to Bun 1.3.11 or later to fetch from HTTPS servers on non-default ports.`);
171
+ return (0, node_net.isIP)(hostname) === 0 ? { serverName: hostname } : {};
172
+ }
173
+ /**
174
+ * Combines two signals without `AbortSignal.any()`, which Bun 1.1.0 lacks.
175
+ */
176
+ function anySignal(a, b) {
177
+ if (a.aborted) return a;
178
+ if (b.aborted) return b;
179
+ const controller = new AbortController();
180
+ const abort = (event) => {
181
+ controller.abort(event.target.reason);
182
+ a.removeEventListener("abort", abort);
183
+ b.removeEventListener("abort", abort);
184
+ };
185
+ a.addEventListener("abort", abort);
186
+ b.addEventListener("abort", abort);
187
+ return controller.signal;
188
+ }
189
+ function toAddressUrl(url, address) {
190
+ const result = new URL(url);
191
+ result.hostname = address.family === 6 ? `[${address.address}]` : address.address;
192
+ return result;
193
+ }
194
+ function isFallbackError(error) {
195
+ if (error instanceof Error && error.name === "AbortError") return false;
196
+ if (error instanceof TypeError) return true;
197
+ const code = error?.code;
198
+ return typeof code === "string" && FALLBACK_ERROR_CODES.has(code);
199
+ }
200
+ function throwIfAborted(...signals) {
201
+ for (const signal of signals) signal?.throwIfAborted();
202
+ }
203
+ function getBun() {
204
+ return globalThis.Bun;
205
+ }
206
+ function ignoresProxyFalse() {
207
+ const bun = getBun();
208
+ if (bun == null) return false;
209
+ return !bun.semver.satisfies(bun.version, `>=${PROXY_FALSE_MIN_VERSION}`);
210
+ }
211
+ function hasProxyVariables() {
212
+ const env = globalThis.process?.env;
213
+ if (env == null) return false;
214
+ return PROXY_VARIABLES.some((name) => (env[name] ?? "") !== "");
215
+ }
216
+ //#endregion
217
+ //#region src/pinners/node.ts
218
+ /**
219
+ * Creates a pinner for Node.js. Every request is sent through the global
220
+ * `fetch()` with an undici `Agent` as the `dispatcher`. The agent's
221
+ * `connect.lookup` answers only with the addresses of live grants for
222
+ * the hostname, checking each of them again right before connecting, and
223
+ * fails when there are none, so a connection can never be made to an address
224
+ * that was not validated.
225
+ *
226
+ * The agent is shared by the requests of all grants, so keep-alive
227
+ * connections are reused. A pooled connection was made to an address of
228
+ * a grant that was valid when it was connected, so reusing it from another
229
+ * session of the same origin still reaches a validated address. Response
230
+ * bodies have to be consumed or cancelled for their connections to return to
231
+ * the pool, which sessions do when they are closed.
232
+ *
233
+ * Since the dispatcher is passed explicitly, neither proxy environment
234
+ * variables nor the global dispatcher (e.g., `NODE_USE_ENV_PROXY=1`) apply.
235
+ * @param options The pinner options.
236
+ * @returns The pinner.
237
+ */
238
+ function createNodePinner({ verifyAddress, undici }) {
239
+ const grants = /* @__PURE__ */ new Map();
240
+ function lookup(hostname, options, callback) {
241
+ let addresses;
242
+ try {
243
+ addresses = getGrantedAddresses(hostname, options.family);
244
+ } catch (error) {
245
+ callback(error, []);
246
+ return;
247
+ }
248
+ if (options.all) callback(null, addresses);
249
+ else callback(null, addresses[0].address, addresses[0].family);
250
+ }
251
+ function getGrantedAddresses(hostname, family) {
252
+ const addresses = /* @__PURE__ */ new Map();
253
+ for (const [grant, record] of grants.get(hostname) ?? []) {
254
+ try {
255
+ redeemPinGrant(grant, new URL(record.origin));
256
+ } catch {
257
+ continue;
258
+ }
259
+ for (const address of record.addresses) addresses.set(address.address, address);
260
+ }
261
+ const familyNumber = family === "IPv4" ? 4 : family === "IPv6" ? 6 : typeof family === "number" ? family : 0;
262
+ const result = [...addresses.values()].filter((address) => familyNumber === 0 || address.family === familyNumber);
263
+ if (result.length < 1) throw new require_url.UrlError(`No validated address to connect to: ${hostname}`);
264
+ for (const address of result) verifyAddress(address);
265
+ return result;
266
+ }
267
+ const agent = new undici.Agent({ connect: { lookup } });
268
+ return {
269
+ supports(url) {
270
+ return url.protocol === "http:" || url.protocol === "https:";
271
+ },
272
+ async fetch(grant, request, signal) {
273
+ const record = redeemPinGrant(grant, new URL(request.url));
274
+ if ((0, node_net.isIP)(record.hostname) !== 0) for (const address of record.addresses) verifyAddress(address);
275
+ let registered = grants.get(record.hostname);
276
+ if (registered == null) {
277
+ registered = /* @__PURE__ */ new Map();
278
+ grants.set(record.hostname, registered);
279
+ }
280
+ registered.set(grant, record);
281
+ return await fetch(request, {
282
+ redirect: "manual",
283
+ signal: signal == null || signal === request.signal ? request.signal : AbortSignal.any([signal, request.signal]),
284
+ dispatcher: agent
285
+ });
286
+ },
287
+ release(grant) {
288
+ for (const [hostname, registered] of grants) {
289
+ if (!registered.delete(grant)) continue;
290
+ if (registered.size < 1) grants.delete(hostname);
291
+ break;
292
+ }
293
+ }
294
+ };
295
+ }
296
+ //#endregion
297
+ //#region src/pinners/mod.ts
298
+ /**
299
+ * Detects the current runtime. Cloudflare Workers is checked first, since
300
+ * `process` can exist there with `nodejs_compat`, and Deno and Bun before
301
+ * Node.js, since both provide `process.versions.node`.
302
+ * @returns The current runtime.
303
+ */
304
+ function detectRuntime() {
305
+ const g = globalThis;
306
+ if (g.navigator?.userAgent === "Cloudflare-Workers") return "cfworkers";
307
+ if ("Deno" in globalThis) return "deno";
308
+ if ("Bun" in globalThis) return "bun";
309
+ if (g.process?.versions?.node != null) return "node";
310
+ return "other";
311
+ }
312
+ let runtimePinner;
313
+ /**
314
+ * Gets the production pinner of the current runtime, which rejects
315
+ * non-public addresses right before connecting. Deno and Cloudflare Workers
316
+ * have no pinner.
317
+ * @returns The pinner, or `null` if the runtime has none.
318
+ */
319
+ function getRuntimePinner() {
320
+ runtimePinner ??= createRuntimePinner(detectRuntime());
321
+ return runtimePinner;
322
+ }
323
+ async function createRuntimePinner(runtime) {
324
+ switch (runtime) {
325
+ case "bun": return createBunPinner({ verifyAddress: require_url.assertPublicAddress });
326
+ case "node": return createNodePinner({
327
+ verifyAddress: require_url.assertPublicAddress,
328
+ undici: await import("undici")
329
+ });
330
+ default: return null;
331
+ }
332
+ }
333
+ //#endregion
334
+ //#region src/public-fetch.ts
335
+ const logger = (0, _logtape_logtape.getLogger)([
336
+ "fedify",
337
+ "runtime",
338
+ "fetch"
339
+ ]);
340
+ /**
341
+ * Creates {@link PublicFetch.openPublicFetch} and
342
+ * {@link PublicFetch.withPublicFetch} bound to the given pinner and lookup.
343
+ * This is exported only for tests; it is not exported from any entry point.
344
+ * @param dependencies The pinner and the default lookup function.
345
+ * @returns The functions.
346
+ */
347
+ function createPublicFetch(dependencies) {
348
+ async function openPublicFetch(url, options = {}) {
349
+ const { url: parsed, hostname, family } = require_url.parsePublicUrl(url);
350
+ if (parsed.username !== "" || parsed.password !== "") throw new require_url.UrlError("URLs with credentials are not allowed");
351
+ const lookup = options.lookup ?? dependencies.lookup;
352
+ let addresses;
353
+ if (family !== 0) addresses = [{
354
+ address: hostname,
355
+ family
356
+ }];
357
+ else if (lookup === require_url.lookupAllAddresses && !await canResolve()) return createSession(parsed, globalFetch);
358
+ else {
359
+ options.signal?.throwIfAborted();
360
+ addresses = await require_url.lookupPublicAddresses(hostname, lookup);
361
+ options.signal?.throwIfAborted();
362
+ }
363
+ const pinner = await dependencies.pinner();
364
+ if (pinner == null || !pinner.supports(parsed)) {
365
+ logUnpinned(parsed);
366
+ return createSession(parsed, globalFetch);
367
+ }
368
+ const grant = issuePinGrant(parsed, addresses);
369
+ return createSession(parsed, (input, init) => pinner.fetch(grant, new Request(input, init), init.signal ?? void 0), {
370
+ grant,
371
+ pinner
372
+ });
373
+ }
374
+ async function withPublicFetch(url, fn, options) {
375
+ const session = await openPublicFetch(url, options);
376
+ try {
377
+ const result = await fn(session.fetch);
378
+ if (result instanceof Response) throw new TypeError("withPublicFetch() callbacks must not return a Response, since its body is cancelled when the session is closed.");
379
+ return result;
380
+ } catch (error) {
381
+ logger.debug("Failed while fetching {url}; closing the session: {error}", {
382
+ url: session.url.href,
383
+ error
384
+ });
385
+ throw error;
386
+ } finally {
387
+ await session.close();
388
+ }
389
+ }
390
+ return {
391
+ openPublicFetch,
392
+ withPublicFetch
393
+ };
394
+ }
395
+ const runtimePublicFetch = createPublicFetch({
396
+ pinner: getRuntimePinner,
397
+ lookup: require_url.lookupAllAddresses
398
+ });
399
+ /**
400
+ * Validates the URL, resolves its hostname, checks that every resolved
401
+ * address is public, and opens a session that connects only to those
402
+ * addresses. Connections are pinned on Node.js and Bun. On other runtimes,
403
+ * the session uses the global `fetch()` after the check.
404
+ * @param url The URL to open a session for.
405
+ * @param options The options.
406
+ * @returns The session. It must be closed.
407
+ * @throws {UrlError} If the URL is not allowed, or its hostname cannot be
408
+ * resolved (with `reason: "dns"`).
409
+ */
410
+ const openPublicFetch = runtimePublicFetch.openPublicFetch;
411
+ /**
412
+ * Opens a session with {@link openPublicFetch}, runs the given function with
413
+ * its `fetch()`, and closes the session afterwards.
414
+ * @param url The URL to open a session for.
415
+ * @param fn The function to run. It must not return a `Response`, since
416
+ * the response bodies are cancelled when the session is closed.
417
+ * @param options The options.
418
+ * @returns The value returned by `fn`.
419
+ * @throws {UrlError} If the URL is not allowed.
420
+ * @throws {TypeError} If `fn` returns a `Response`.
421
+ */
422
+ const withPublicFetch = runtimePublicFetch.withPublicFetch;
423
+ /**
424
+ * Opens a session that neither validates nor pins addresses, for callers that
425
+ * allow private addresses. It has the same rules as {@link openPublicFetch}
426
+ * sessions otherwise: requests are limited to the URL's origin, redirects are
427
+ * never followed, and unconsumed response bodies are cancelled on close.
428
+ * It grants nothing beyond calling the global `fetch()` directly.
429
+ * @param url The URL to open a session for.
430
+ * @returns The session. It must be closed.
431
+ */
432
+ function openDirectFetch(url) {
433
+ return Promise.resolve(createSession(new URL(url), globalFetch));
434
+ }
435
+ /**
436
+ * Starts cancelling the body of a response without waiting for it to settle.
437
+ * The cancellation of a branch of a tee (e.g., a body cloned by a fetch mock)
438
+ * settles only after the other branch is cancelled too, per the Streams
439
+ * standard, so awaiting it could block forever. Failures are only logged.
440
+ */
441
+ function cancelResponseBody(response, url) {
442
+ response.body?.cancel().catch((error) => {
443
+ logger.debug("Failed to cancel a response body from {url}: {error}", {
444
+ url: url.href,
445
+ error
446
+ });
447
+ });
448
+ }
449
+ function globalFetch(input, init) {
450
+ return fetch(input, init);
451
+ }
452
+ function createSession(url, send, pin) {
453
+ const origin = url.origin;
454
+ const tracked = [];
455
+ let closed = false;
456
+ async function sessionFetch(input, init) {
457
+ if (closed) throw new TypeError("The fetch session is closed.");
458
+ const target = new URL(input instanceof Request ? input.url : input);
459
+ if (target.origin !== origin) throw new TypeError(`A fetch session for ${origin} cannot fetch ${target.origin}.`);
460
+ const response = await send(input, {
461
+ ...init,
462
+ redirect: "manual"
463
+ });
464
+ if (closed) {
465
+ cancelResponseBody(response, url);
466
+ throw new TypeError("The fetch session is closed.");
467
+ }
468
+ tracked.push(response);
469
+ return response;
470
+ }
471
+ async function close() {
472
+ if (closed) return;
473
+ closed = true;
474
+ for (const response of tracked) {
475
+ if (response.body == null || response.bodyUsed) continue;
476
+ if (response.body.locked) {
477
+ logger.debug("Cannot cancel a locked response body from {url}.", { url: url.href });
478
+ continue;
479
+ }
480
+ cancelResponseBody(response, url);
481
+ }
482
+ if (pin == null) return;
483
+ revokePinGrant(pin.grant);
484
+ await pin.pinner.release(pin.grant);
485
+ }
486
+ return {
487
+ get url() {
488
+ return new URL(url);
489
+ },
490
+ fetch: sessionFetch,
491
+ close
492
+ };
493
+ }
494
+ async function canResolve() {
495
+ if (!("Deno" in globalThis)) return true;
496
+ return (await Deno.permissions.query({ name: "net" })).state === "granted";
497
+ }
498
+ const loggedUnpinned = /* @__PURE__ */ new Set();
499
+ function logUnpinned(url) {
500
+ const runtime = detectRuntime();
501
+ const key = `${runtime} ${url.protocol}`;
502
+ if (loggedUnpinned.has(key)) return;
503
+ loggedUnpinned.add(key);
504
+ logger.debug("Outbound {protocol} connections are not pinned to validated addresses on {runtime}; the hostname is resolved again when connecting.", {
505
+ runtime,
506
+ protocol: url.protocol
507
+ });
508
+ }
509
+ //#endregion
510
+ Object.defineProperty(exports, "openDirectFetch", {
511
+ enumerable: true,
512
+ get: function() {
513
+ return openDirectFetch;
514
+ }
515
+ });
516
+ Object.defineProperty(exports, "openPublicFetch", {
517
+ enumerable: true,
518
+ get: function() {
519
+ return openPublicFetch;
520
+ }
521
+ });
522
+ Object.defineProperty(exports, "withPublicFetch", {
523
+ enumerable: true,
524
+ get: function() {
525
+ return withPublicFetch;
526
+ }
527
+ });