@fedify/vocab-runtime 2.4.5-dev.2462 → 2.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/deno.json +5 -2
  2. package/dist/{docloader-BMHljyYb.d.cts → docloader-DOWjnNCz.d.cts} +4 -8
  3. package/dist/{docloader-BMHljyYb.d.ts → docloader-DOWjnNCz.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 +1 -1
  9. package/dist/internal/portable-dereference.d.cts +2 -2
  10. package/dist/internal/portable-dereference.d.ts +2 -2
  11. package/dist/internal/portable-dereference.js +1 -1
  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-6Aj-7dZJ.d.cts → portable-CTnZWX7J.d.cts} +1 -1
  21. package/dist/{portable-nkTiqH0w.d.ts → portable-Cq-rhQgY.d.ts} +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-Dlnsb-QX.cjs → body-BO79kuv1.cjs} +1 -1
  25. package/dist/tests/{body-BXrFIcFa.mjs → body-CUzCsDZ4.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 +3 -3
  29. package/dist/tests/decimal.test.mjs +3 -3
  30. package/dist/tests/{docloader-c9BQWaF1.cjs → docloader-B79_YgeY.cjs} +111 -39
  31. package/dist/tests/{docloader-DsuF8YDf.mjs → docloader-C5ZHGEfO.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-BvS3AhZ2.cjs +89 -0
  43. package/dist/tests/node-Cg4pRuYy.mjs +84 -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-CJi9ZkYs.cjs +220 -0
  67. package/dist/tests/pinners-zSofbiy2.mjs +203 -0
  68. package/dist/tests/{portable-dereference-DmTYDy2p.mjs → portable-dereference-BHRE5Pkm.mjs} +2 -2
  69. package/dist/tests/{portable-dereference-qTLMqe2_.cjs → portable-dereference-mRHrC7PI.cjs} +2 -2
  70. package/dist/tests/{portable-media-M7idvaUq.mjs → portable-media-BpVpe1gj.mjs} +3 -3
  71. package/dist/tests/{portable-media-CS4czG6X.cjs → portable-media-C3yY0W5E.cjs} +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-DNkPoks1.mjs +170 -0
  77. package/dist/tests/public-fetch-U822jbKB.cjs +186 -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-C336eT6A.mjs → request-3AP-HBJ6.mjs} +1 -1
  83. package/dist/tests/{request-CgTgsBUA.cjs → request-BhopJTZd.cjs} +1 -1
  84. package/dist/tests/request.test.cjs +1 -1
  85. package/dist/tests/request.test.mjs +1 -1
  86. package/dist/tests/support-BmzI3dRJ.cjs +363 -0
  87. package/dist/tests/support-CiHDVJgh.mjs +272 -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.ts +13 -0
  100. package/src/pin-grant.test.ts +118 -0
  101. package/src/pin-grant.ts +99 -0
  102. package/src/pinners/bun.test.ts +278 -0
  103. package/src/pinners/bun.ts +223 -0
  104. package/src/pinners/mod.ts +59 -0
  105. package/src/pinners/node.test.ts +415 -0
  106. package/src/pinners/node.ts +144 -0
  107. package/src/pinners/pinners.test.ts +316 -0
  108. package/src/pinners/testdata/node-child.ts +147 -0
  109. package/src/pinners/testdata/proxy-child.ts +82 -0
  110. package/src/pinners/testdata/support.ts +343 -0
  111. package/src/pinners/testdata/tls-cert.pem +12 -0
  112. package/src/pinners/testdata/tls-child.ts +88 -0
  113. package/src/pinners/testdata/tls-key.pem +5 -0
  114. package/src/pinners/types.ts +48 -0
  115. package/src/public-fetch.test.ts +1286 -0
  116. package/src/public-fetch.ts +338 -0
  117. package/src/testdata/public-fetch-deno-worker.ts +74 -0
  118. package/src/url.test.ts +11 -0
  119. package/src/url.ts +113 -22
  120. package/tsdown.config.ts +7 -2
@@ -0,0 +1,1074 @@
1
+ import { t as esm_default } from "./esm-Ds-eHTfY.mjs";
2
+ import { n as redeemPinGrant } from "./pin-grant-H5XhM1Xs.mjs";
3
+ import { n as UrlError } from "./url-COj5Q8mb.mjs";
4
+ import { t as detectRuntime } from "./pinners-zSofbiy2.mjs";
5
+ import { n as openDirectFetch, r as openPublicFetch, t as createPublicFetch } from "./public-fetch-DNkPoks1.mjs";
6
+ import { configure, reset } from "@logtape/logtape";
7
+ import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert";
8
+ import { test } from "@fedify/fixture";
9
+ import { createServer } from "node:http";
10
+ //#region src/public-fetch.test.ts
11
+ function createRecordingPinner(respond = () => new Response("ok")) {
12
+ const calls = [];
13
+ const grants = [];
14
+ const released = [];
15
+ let releaseError;
16
+ return {
17
+ pinner: {
18
+ supports(url) {
19
+ return url.protocol === "http:" || url.protocol === "https:";
20
+ },
21
+ async fetch(grant, request) {
22
+ const { addresses } = redeemPinGrant(grant, new URL(request.url));
23
+ grants.push(grant);
24
+ const index = calls.length;
25
+ calls.push({
26
+ url: request.url,
27
+ method: request.method,
28
+ redirect: request.redirect,
29
+ addresses
30
+ });
31
+ return await respond(request, index);
32
+ },
33
+ release(grant) {
34
+ released.push(grant);
35
+ if (releaseError != null) throw releaseError;
36
+ }
37
+ },
38
+ calls,
39
+ grants,
40
+ released,
41
+ failRelease(error) {
42
+ releaseError = error;
43
+ }
44
+ };
45
+ }
46
+ function createRecordingLookup(...results) {
47
+ const hostnames = [];
48
+ function lookup(hostname) {
49
+ hostnames.push(hostname);
50
+ const result = results[Math.min(hostnames.length, results.length) - 1];
51
+ return result instanceof Error ? Promise.reject(result) : Promise.resolve(result);
52
+ }
53
+ return {
54
+ lookup,
55
+ hostnames
56
+ };
57
+ }
58
+ const publicAddresses = [{
59
+ address: "8.8.8.8",
60
+ family: 4
61
+ }];
62
+ function failingLookup() {
63
+ return Promise.reject(/* @__PURE__ */ new Error("The default lookup must not be used."));
64
+ }
65
+ function createTrackedBody(options = {}) {
66
+ const state = {
67
+ cancelled: 0,
68
+ cancelReasons: []
69
+ };
70
+ let sent = false;
71
+ return {
72
+ stream: new ReadableStream({
73
+ pull(controller) {
74
+ if (sent) {
75
+ controller.close();
76
+ return;
77
+ }
78
+ sent = true;
79
+ controller.enqueue(new TextEncoder().encode(options.text ?? "body"));
80
+ },
81
+ cancel(reason) {
82
+ state.cancelled++;
83
+ state.cancelReasons.push(reason);
84
+ if (options.cancelError != null) throw options.cancelError;
85
+ }
86
+ }),
87
+ state
88
+ };
89
+ }
90
+ async function captureLogs(fn) {
91
+ const records = [];
92
+ await configure({
93
+ sinks: { buffer: (record) => records.push(record) },
94
+ loggers: [{
95
+ category: [
96
+ "fedify",
97
+ "runtime",
98
+ "fetch"
99
+ ],
100
+ sinks: ["buffer"],
101
+ lowestLevel: "debug"
102
+ }, {
103
+ category: ["logtape", "meta"],
104
+ sinks: []
105
+ }],
106
+ reset: true
107
+ });
108
+ try {
109
+ return {
110
+ result: await fn(),
111
+ records
112
+ };
113
+ } finally {
114
+ await reset();
115
+ }
116
+ }
117
+ /**
118
+ * Waits until the rejections of body cancellations started without being
119
+ * awaited have been handled.
120
+ */
121
+ function settleCancellations() {
122
+ return new Promise((resolve) => setTimeout(resolve, 0));
123
+ }
124
+ /**
125
+ * Fails instead of blocking the test if the given promise does not settle
126
+ * within the given time.
127
+ */
128
+ async function settlesWithin(promise, milliseconds = 1e3) {
129
+ let timer;
130
+ try {
131
+ return await Promise.race([promise, new Promise((_, reject) => {
132
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Did not settle in ${milliseconds} ms.`)), milliseconds);
133
+ })]);
134
+ } finally {
135
+ clearTimeout(timer);
136
+ }
137
+ }
138
+ function cancelFailureLogs(records) {
139
+ return records.filter((record) => record.rawMessage === "Failed to cancel a response body from {url}: {error}").map((record) => [
140
+ record.level,
141
+ record.properties.url,
142
+ record.properties.error
143
+ ]);
144
+ }
145
+ async function withMockedGlobalFetch(fn) {
146
+ esm_default.mockGlobal();
147
+ try {
148
+ return await fn();
149
+ } finally {
150
+ esm_default.hardReset();
151
+ }
152
+ }
153
+ async function listen(server, host = "127.0.0.1") {
154
+ await new Promise((resolve) => server.listen(0, host, resolve));
155
+ return server.address().port;
156
+ }
157
+ async function closeServer(server) {
158
+ server.closeAllConnections?.();
159
+ await new Promise((resolve) => server.close(() => resolve()));
160
+ }
161
+ test("openPublicFetch() pins IP literal hostnames without lookups", async () => {
162
+ const { lookup, hostnames } = createRecordingLookup(publicAddresses);
163
+ const recorder = createRecordingPinner();
164
+ const { openPublicFetch } = createPublicFetch({
165
+ pinner: () => Promise.resolve(recorder.pinner),
166
+ lookup: failingLookup
167
+ });
168
+ for (const [url, address] of [["https://8.8.8.8/", {
169
+ address: "8.8.8.8",
170
+ family: 4
171
+ }], ["https://[2001:4860:4860::8888]:8443/", {
172
+ address: "2001:4860:4860::8888",
173
+ family: 6
174
+ }]]) {
175
+ const session = await openPublicFetch(url, { lookup });
176
+ try {
177
+ deepStrictEqual(await (await session.fetch(url)).text(), "ok");
178
+ } finally {
179
+ await session.close();
180
+ }
181
+ deepStrictEqual(recorder.calls.at(-1)?.addresses, [address]);
182
+ }
183
+ deepStrictEqual(hostnames, []);
184
+ });
185
+ test("openPublicFetch() pins the addresses the lookup returned", async () => {
186
+ const { lookup, hostnames } = createRecordingLookup([
187
+ {
188
+ address: "8.8.8.8",
189
+ family: 4
190
+ },
191
+ {
192
+ address: "cname-target.invalid",
193
+ family: 0
194
+ },
195
+ {
196
+ address: "2001:4860:4860::8888",
197
+ family: 6
198
+ }
199
+ ]);
200
+ const recorder = createRecordingPinner();
201
+ const { openPublicFetch } = createPublicFetch({
202
+ pinner: () => Promise.resolve(recorder.pinner),
203
+ lookup: failingLookup
204
+ });
205
+ const session = await openPublicFetch("https://PINNED-HOST.invalid/object", { lookup });
206
+ try {
207
+ deepStrictEqual(session.url.href, "https://pinned-host.invalid/object");
208
+ session.url.pathname = "/changed";
209
+ deepStrictEqual(session.url.href, "https://pinned-host.invalid/object");
210
+ deepStrictEqual(await (await session.fetch("https://pinned-host.invalid/object")).text(), "ok");
211
+ } finally {
212
+ await session.close();
213
+ }
214
+ deepStrictEqual(hostnames, ["pinned-host.invalid"]);
215
+ deepStrictEqual(recorder.calls, [{
216
+ url: "https://pinned-host.invalid/object",
217
+ method: "GET",
218
+ redirect: "manual",
219
+ addresses: [{
220
+ address: "8.8.8.8",
221
+ family: 4
222
+ }, {
223
+ address: "2001:4860:4860::8888",
224
+ family: 6
225
+ }]
226
+ }]);
227
+ deepStrictEqual(recorder.released, recorder.grants.slice(0, 1));
228
+ });
229
+ test("openPublicFetch() uses the default lookup without options", async () => {
230
+ const { lookup, hostnames } = createRecordingLookup(publicAddresses);
231
+ const recorder = createRecordingPinner();
232
+ const { openPublicFetch } = createPublicFetch({
233
+ pinner: () => Promise.resolve(recorder.pinner),
234
+ lookup
235
+ });
236
+ await (await openPublicFetch("https://pinned-host.invalid/")).close();
237
+ deepStrictEqual(hostnames, ["pinned-host.invalid"]);
238
+ });
239
+ test("openPublicFetch() rejects disallowed URLs", async (t) => {
240
+ const cases = [
241
+ {
242
+ name: "protocol",
243
+ url: "ftp://pinned-host.invalid/",
244
+ reason: "disallowed"
245
+ },
246
+ {
247
+ name: "localhost",
248
+ url: "https://localhost/",
249
+ reason: "disallowed"
250
+ },
251
+ {
252
+ name: "IPv4 loopback",
253
+ url: "https://127.0.0.1/",
254
+ reason: "disallowed"
255
+ },
256
+ {
257
+ name: "IPv6 loopback",
258
+ url: "https://[::1]/",
259
+ reason: "disallowed"
260
+ },
261
+ {
262
+ name: "IPv4-mapped IPv6",
263
+ url: "https://[::ffff:7f00:1]/",
264
+ reason: "disallowed"
265
+ },
266
+ {
267
+ name: "credentials",
268
+ url: "https://user:password@pinned-host.invalid/",
269
+ lookup: publicAddresses,
270
+ reason: "disallowed"
271
+ },
272
+ {
273
+ name: "username",
274
+ url: "https://user@pinned-host.invalid/",
275
+ lookup: publicAddresses,
276
+ reason: "disallowed"
277
+ },
278
+ {
279
+ name: "private lookup result",
280
+ url: "https://pinned-host.invalid/",
281
+ lookup: [{
282
+ address: "10.0.0.1",
283
+ family: 4
284
+ }],
285
+ reason: "disallowed"
286
+ },
287
+ {
288
+ name: "mixed lookup result",
289
+ url: "https://pinned-host.invalid/",
290
+ lookup: [{
291
+ address: "8.8.8.8",
292
+ family: 4
293
+ }, {
294
+ address: "127.0.0.1",
295
+ family: 4
296
+ }],
297
+ reason: "disallowed"
298
+ },
299
+ {
300
+ name: "mixed IPv6 lookup result",
301
+ url: "https://pinned-host.invalid/",
302
+ lookup: [{
303
+ address: "2001:4860:4860::8888",
304
+ family: 6
305
+ }, {
306
+ address: "fd00::1",
307
+ family: 6
308
+ }],
309
+ reason: "disallowed"
310
+ },
311
+ {
312
+ name: "lookup failure",
313
+ url: "https://pinned-host.invalid/",
314
+ lookup: Object.assign(/* @__PURE__ */ new Error("getaddrinfo ENOTFOUND"), { code: "ENOTFOUND" }),
315
+ reason: "dns"
316
+ },
317
+ {
318
+ name: "empty lookup result",
319
+ url: "https://pinned-host.invalid/",
320
+ lookup: [],
321
+ reason: "dns"
322
+ },
323
+ {
324
+ name: "CNAME-only lookup result",
325
+ url: "https://pinned-host.invalid/",
326
+ lookup: [{
327
+ address: "cname-target.invalid",
328
+ family: 0
329
+ }],
330
+ reason: "dns"
331
+ }
332
+ ];
333
+ for (const { name, url, lookup: result, reason } of cases) await t.step(name, async () => {
334
+ const { lookup } = createRecordingLookup(result ?? publicAddresses);
335
+ const recorder = createRecordingPinner();
336
+ let pinnerRequested = 0;
337
+ const { openPublicFetch } = createPublicFetch({
338
+ pinner: () => {
339
+ pinnerRequested++;
340
+ return Promise.resolve(recorder.pinner);
341
+ },
342
+ lookup: failingLookup
343
+ });
344
+ await withMockedGlobalFetch(async () => {
345
+ esm_default.catch(500);
346
+ await rejects(() => openPublicFetch(url, { lookup }), (error) => {
347
+ ok(error instanceof UrlError, String(error));
348
+ deepStrictEqual(error.reason, reason);
349
+ return true;
350
+ });
351
+ deepStrictEqual(esm_default.callHistory.calls(), []);
352
+ });
353
+ deepStrictEqual(pinnerRequested, 0);
354
+ deepStrictEqual(recorder.calls, []);
355
+ deepStrictEqual(recorder.released, []);
356
+ });
357
+ });
358
+ test("openPublicFetch() resists DNS rebinding", async () => {
359
+ const { lookup, hostnames } = createRecordingLookup([{
360
+ address: "8.8.8.8",
361
+ family: 4
362
+ }], [{
363
+ address: "127.0.0.1",
364
+ family: 4
365
+ }]);
366
+ const recorder = createRecordingPinner();
367
+ const { openPublicFetch, withPublicFetch } = createPublicFetch({
368
+ pinner: () => Promise.resolve(recorder.pinner),
369
+ lookup: failingLookup
370
+ });
371
+ const session = await openPublicFetch("https://rebinding.invalid/", { lookup });
372
+ try {
373
+ for (const input of [
374
+ "https://rebinding.invalid/a",
375
+ new URL("https://rebinding.invalid/b"),
376
+ new Request("https://rebinding.invalid/c", { method: "POST" }),
377
+ "https://rebinding.invalid/a"
378
+ ]) await (await session.fetch(input)).body?.cancel();
379
+ } finally {
380
+ await session.close();
381
+ }
382
+ deepStrictEqual(hostnames, ["rebinding.invalid"]);
383
+ deepStrictEqual(recorder.calls.map(({ url, method, addresses }) => [
384
+ url,
385
+ method,
386
+ addresses
387
+ ]), [
388
+ [
389
+ "https://rebinding.invalid/a",
390
+ "GET",
391
+ [{
392
+ address: "8.8.8.8",
393
+ family: 4
394
+ }]
395
+ ],
396
+ [
397
+ "https://rebinding.invalid/b",
398
+ "GET",
399
+ [{
400
+ address: "8.8.8.8",
401
+ family: 4
402
+ }]
403
+ ],
404
+ [
405
+ "https://rebinding.invalid/c",
406
+ "POST",
407
+ [{
408
+ address: "8.8.8.8",
409
+ family: 4
410
+ }]
411
+ ],
412
+ [
413
+ "https://rebinding.invalid/a",
414
+ "GET",
415
+ [{
416
+ address: "8.8.8.8",
417
+ family: 4
418
+ }]
419
+ ]
420
+ ]);
421
+ deepStrictEqual(new Set(recorder.grants).size, 1);
422
+ await rejects(() => withPublicFetch("https://rebinding.invalid/", () => Promise.reject(/* @__PURE__ */ new Error("must not be called")), { lookup }), UrlError);
423
+ deepStrictEqual(hostnames, ["rebinding.invalid", "rebinding.invalid"]);
424
+ deepStrictEqual(recorder.calls.length, 4);
425
+ });
426
+ test("PublicFetchSession.fetch() rejects other origins", async () => {
427
+ const { lookup } = createRecordingLookup(publicAddresses);
428
+ const recorder = createRecordingPinner();
429
+ const { openPublicFetch } = createPublicFetch({
430
+ pinner: () => Promise.resolve(recorder.pinner),
431
+ lookup: failingLookup
432
+ });
433
+ const session = await openPublicFetch("https://pinned-host.invalid/a", { lookup });
434
+ try {
435
+ for (const input of [
436
+ "http://pinned-host.invalid/a",
437
+ "https://pinned-host.invalid:8443/a",
438
+ "https://other-host.invalid/a",
439
+ "https://8.8.8.8/a",
440
+ new URL("https://other-host.invalid/a"),
441
+ new Request("https://other-host.invalid/a")
442
+ ]) await rejects(() => session.fetch(input), TypeError, String(input));
443
+ deepStrictEqual(recorder.calls, []);
444
+ await (await session.fetch("https://pinned-host.invalid/b?c")).body?.cancel();
445
+ deepStrictEqual(recorder.calls.map(({ url }) => url), ["https://pinned-host.invalid/b?c"]);
446
+ } finally {
447
+ await session.close();
448
+ }
449
+ });
450
+ test("PublicFetchSession.fetch() never follows redirects", async (t) => {
451
+ await t.step("pinned", async () => {
452
+ const { lookup } = createRecordingLookup(publicAddresses);
453
+ const recorder = createRecordingPinner(() => new Response(null, {
454
+ status: 302,
455
+ headers: { Location: "https://pinned-host.invalid/target" }
456
+ }));
457
+ const { openPublicFetch } = createPublicFetch({
458
+ pinner: () => Promise.resolve(recorder.pinner),
459
+ lookup: failingLookup
460
+ });
461
+ const session = await openPublicFetch("https://pinned-host.invalid/", { lookup });
462
+ try {
463
+ const url = "https://pinned-host.invalid/";
464
+ for (const [input, init] of [
465
+ [url, void 0],
466
+ [url, { redirect: "follow" }],
467
+ [url, { redirect: "error" }],
468
+ [new Request(url), void 0],
469
+ [new Request(url, { redirect: "follow" }), void 0],
470
+ [new Request(url), { redirect: "follow" }]
471
+ ]) deepStrictEqual((await session.fetch(input, init)).status, 302);
472
+ } finally {
473
+ await session.close();
474
+ }
475
+ deepStrictEqual(recorder.calls.map(({ redirect }) => redirect), Array(6).fill("manual"));
476
+ });
477
+ await t.step("unpinned", async () => {
478
+ const { lookup } = createRecordingLookup(publicAddresses);
479
+ const { openPublicFetch } = createPublicFetch({
480
+ pinner: () => Promise.resolve(null),
481
+ lookup: failingLookup
482
+ });
483
+ await withMockedGlobalFetch(async () => {
484
+ esm_default.route("https://unpinned-host.invalid/", 200);
485
+ const session = await openPublicFetch("https://unpinned-host.invalid/", { lookup });
486
+ try {
487
+ await session.fetch("https://unpinned-host.invalid/", { redirect: "follow" });
488
+ await session.fetch(new Request("https://unpinned-host.invalid/", { redirect: "follow" }));
489
+ } finally {
490
+ await session.close();
491
+ }
492
+ const calls = esm_default.callHistory.calls();
493
+ deepStrictEqual(calls.length, 2);
494
+ for (const call of calls) deepStrictEqual(call.args[1].redirect, "manual");
495
+ });
496
+ });
497
+ await t.step("real server", async () => {
498
+ let hits = 0;
499
+ const server = createServer((_, res) => {
500
+ hits++;
501
+ res.writeHead(302, { Location: "/target" });
502
+ res.end("redirect");
503
+ });
504
+ const port = await listen(server);
505
+ try {
506
+ const url = `http://127.0.0.1:${port}/`;
507
+ const session = await openDirectFetch(url);
508
+ try {
509
+ const response = await session.fetch(url, { redirect: "follow" });
510
+ deepStrictEqual(response.status, 302);
511
+ deepStrictEqual(response.headers.get("Location"), "/target");
512
+ await response.body?.cancel();
513
+ } finally {
514
+ await session.close();
515
+ }
516
+ deepStrictEqual(hits, 1);
517
+ } finally {
518
+ await closeServer(server);
519
+ }
520
+ });
521
+ });
522
+ test("PublicFetchSession.close()", async () => {
523
+ const bodies = [
524
+ createTrackedBody(),
525
+ createTrackedBody(),
526
+ createTrackedBody()
527
+ ];
528
+ const { lookup } = createRecordingLookup(publicAddresses);
529
+ const recorder = createRecordingPinner((_, index) => index < bodies.length ? new Response(bodies[index].stream) : new Response(null, { status: 204 }));
530
+ const { openPublicFetch } = createPublicFetch({
531
+ pinner: () => Promise.resolve(recorder.pinner),
532
+ lookup: failingLookup
533
+ });
534
+ const url = "https://pinned-host.invalid/";
535
+ const session = await openPublicFetch(url, { lookup });
536
+ deepStrictEqual(await (await session.fetch(url)).text(), "body");
537
+ const unconsumed = await session.fetch(url);
538
+ const reader = (await session.fetch(url)).body.getReader();
539
+ deepStrictEqual((await session.fetch(url)).body, null);
540
+ await session.close();
541
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [
542
+ 0,
543
+ 1,
544
+ 0
545
+ ]);
546
+ ok(unconsumed.bodyUsed);
547
+ deepStrictEqual(recorder.released, [recorder.grants[0]]);
548
+ throws(() => redeemPinGrant(recorder.grants[0], new URL(url)), TypeError);
549
+ await rejects(() => session.fetch(url), TypeError);
550
+ deepStrictEqual(recorder.calls.length, 4);
551
+ await session.close();
552
+ deepStrictEqual(recorder.released.length, 1);
553
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [
554
+ 0,
555
+ 1,
556
+ 0
557
+ ]);
558
+ await reader.cancel();
559
+ });
560
+ test("PublicFetchSession.close() logs cancel failures without throwing", async (t) => {
561
+ const url = "https://pinned-host.invalid/";
562
+ function setUp(cancelErrors) {
563
+ const bodies = cancelErrors.map((cancelError) => createTrackedBody({ cancelError }));
564
+ const { lookup } = createRecordingLookup(publicAddresses);
565
+ const recorder = createRecordingPinner((_, index) => new Response(bodies[index].stream));
566
+ const { openPublicFetch } = createPublicFetch({
567
+ pinner: () => Promise.resolve(recorder.pinner),
568
+ lookup: failingLookup
569
+ });
570
+ return {
571
+ bodies,
572
+ recorder,
573
+ lookup,
574
+ openPublicFetch
575
+ };
576
+ }
577
+ async function assertClosed(session, recorder) {
578
+ deepStrictEqual(recorder.released.length, 1);
579
+ for (const grant of recorder.grants) strictEqual(grant, recorder.released[0]);
580
+ throws(() => redeemPinGrant(recorder.released[0], new URL(url)), TypeError);
581
+ await rejects(() => session.fetch(url), TypeError);
582
+ await session.close();
583
+ deepStrictEqual(recorder.released.length, 1);
584
+ }
585
+ await t.step("two failures", async () => {
586
+ const errors = [/* @__PURE__ */ new Error("cancel 1"), /* @__PURE__ */ new Error("cancel 2")];
587
+ const { bodies, recorder, lookup, openPublicFetch } = setUp(errors);
588
+ const { records } = await captureLogs(async () => {
589
+ const session = await openPublicFetch(url, { lookup });
590
+ await session.fetch(url);
591
+ await session.fetch(url);
592
+ await session.close();
593
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [1, 1]);
594
+ await assertClosed(session, recorder);
595
+ await settleCancellations();
596
+ });
597
+ deepStrictEqual(cancelFailureLogs(records), [[
598
+ "debug",
599
+ url,
600
+ errors[0]
601
+ ], [
602
+ "debug",
603
+ url,
604
+ errors[1]
605
+ ]]);
606
+ });
607
+ await t.step("one failure", async () => {
608
+ const error = /* @__PURE__ */ new Error("cancel");
609
+ const { bodies, recorder, lookup, openPublicFetch } = setUp([void 0, error]);
610
+ const { records } = await captureLogs(async () => {
611
+ const session = await openPublicFetch(url, { lookup });
612
+ await session.fetch(url);
613
+ await session.fetch(url);
614
+ await session.close();
615
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [1, 1]);
616
+ await assertClosed(session, recorder);
617
+ await settleCancellations();
618
+ });
619
+ deepStrictEqual(cancelFailureLogs(records), [[
620
+ "debug",
621
+ url,
622
+ error
623
+ ]]);
624
+ });
625
+ await t.step("failure already seen by the caller", async () => {
626
+ const error = /* @__PURE__ */ new Error("cancel");
627
+ const { bodies, recorder, lookup, openPublicFetch } = setUp([error]);
628
+ const { records } = await captureLogs(async () => {
629
+ const session = await openPublicFetch(url, { lookup });
630
+ const response = await session.fetch(url);
631
+ await rejects(() => response.body.cancel(), (e) => e === error);
632
+ ok(response.bodyUsed);
633
+ await session.close();
634
+ deepStrictEqual(bodies[0].state.cancelled, 1);
635
+ await assertClosed(session, recorder);
636
+ await settleCancellations();
637
+ });
638
+ deepStrictEqual(cancelFailureLogs(records), []);
639
+ });
640
+ await t.step("release failure", async () => {
641
+ const releaseError = /* @__PURE__ */ new Error("release");
642
+ const { recorder, lookup, openPublicFetch } = setUp([]);
643
+ recorder.failRelease(releaseError);
644
+ const session = await openPublicFetch(url, { lookup });
645
+ await rejects(() => session.close(), (e) => e === releaseError);
646
+ await assertClosed(session, recorder);
647
+ });
648
+ await t.step("cancel and release failures", async () => {
649
+ const cancelError = /* @__PURE__ */ new Error("cancel");
650
+ const releaseError = /* @__PURE__ */ new Error("release");
651
+ const { bodies, recorder, lookup, openPublicFetch } = setUp([cancelError]);
652
+ recorder.failRelease(releaseError);
653
+ const { records } = await captureLogs(async () => {
654
+ const session = await openPublicFetch(url, { lookup });
655
+ await session.fetch(url);
656
+ await rejects(() => session.close(), (e) => e === releaseError);
657
+ deepStrictEqual(bodies[0].state.cancelled, 1);
658
+ await assertClosed(session, recorder);
659
+ await settleCancellations();
660
+ });
661
+ deepStrictEqual(cancelFailureLogs(records), [[
662
+ "debug",
663
+ url,
664
+ cancelError
665
+ ]]);
666
+ });
667
+ });
668
+ test("PublicFetchSession.close() does not wait for body cancellation", async (t) => {
669
+ const url = "https://pinned-host.invalid/";
670
+ async function assertCloseSettles(respond) {
671
+ const { lookup } = createRecordingLookup(publicAddresses);
672
+ const recorder = createRecordingPinner(respond);
673
+ const { openPublicFetch } = createPublicFetch({
674
+ pinner: () => Promise.resolve(recorder.pinner),
675
+ lookup: failingLookup
676
+ });
677
+ const session = await openPublicFetch(url, { lookup });
678
+ const response = await session.fetch(url);
679
+ ok(!response.bodyUsed);
680
+ await settlesWithin(session.close());
681
+ ok(response.bodyUsed);
682
+ deepStrictEqual(recorder.released.length, 1);
683
+ throws(() => redeemPinGrant(recorder.grants[0], new URL(url)), TypeError);
684
+ return recorder.calls.length;
685
+ }
686
+ await t.step("never-settling cancel", async () => {
687
+ const cancelled = [];
688
+ deepStrictEqual(await assertCloseSettles(() => new Response(new ReadableStream({ cancel(reason) {
689
+ cancelled.push(reason);
690
+ return new Promise(() => {});
691
+ } }))), 1);
692
+ deepStrictEqual(cancelled.length, 1);
693
+ });
694
+ await t.step("tee branch", async () => {
695
+ const originals = [];
696
+ deepStrictEqual(await assertCloseSettles(() => {
697
+ const original = new Response(createTrackedBody().stream);
698
+ originals.push(original);
699
+ return original.clone();
700
+ }), 1);
701
+ ok(!originals[0].bodyUsed);
702
+ await originals[0].body.cancel();
703
+ });
704
+ await t.step("fetch-mock responses", async () => {
705
+ const { lookup } = createRecordingLookup(publicAddresses);
706
+ const { openPublicFetch } = createPublicFetch({
707
+ pinner: () => Promise.resolve(null),
708
+ lookup: failingLookup
709
+ });
710
+ await withMockedGlobalFetch(async () => {
711
+ esm_default.route("https://unpinned-host.invalid/string", new Response("string"));
712
+ esm_default.route("https://unpinned-host.invalid/stream", () => new Response(createTrackedBody().stream));
713
+ esm_default.route("https://unpinned-host.invalid/redirect", () => new Response(createTrackedBody().stream, {
714
+ status: 302,
715
+ headers: { Location: "https://unpinned-host.invalid/string" }
716
+ }));
717
+ for (const path of [
718
+ "string",
719
+ "stream",
720
+ "redirect"
721
+ ]) {
722
+ const target = `https://unpinned-host.invalid/${path}`;
723
+ const session = await openPublicFetch(target, { lookup });
724
+ const response = await session.fetch(target);
725
+ ok(!response.bodyUsed);
726
+ await settlesWithin(session.close());
727
+ ok(response.bodyUsed);
728
+ }
729
+ deepStrictEqual(esm_default.callHistory.calls().length, 3);
730
+ });
731
+ });
732
+ });
733
+ test("PublicFetchSession.fetch() cancels responses arriving after close()", async (t) => {
734
+ const url = "https://pinned-host.invalid/";
735
+ async function assertCancelledAfterClose(response) {
736
+ let respond;
737
+ const { lookup } = createRecordingLookup(publicAddresses);
738
+ const recorder = createRecordingPinner(() => new Promise((resolve) => respond = resolve));
739
+ const { openPublicFetch } = createPublicFetch({
740
+ pinner: () => Promise.resolve(recorder.pinner),
741
+ lookup: failingLookup
742
+ });
743
+ const session = await openPublicFetch(url, { lookup });
744
+ const pending = session.fetch(url);
745
+ while (recorder.calls.length < 1) await Promise.resolve();
746
+ await session.close();
747
+ respond(response);
748
+ await settlesWithin(rejects(() => pending, TypeError));
749
+ ok(response.bodyUsed);
750
+ deepStrictEqual(recorder.released.length, 1);
751
+ }
752
+ await t.step("cancellable body", async () => {
753
+ const body = createTrackedBody();
754
+ await assertCancelledAfterClose(new Response(body.stream));
755
+ deepStrictEqual(body.state.cancelled, 1);
756
+ });
757
+ await t.step("never-settling cancel", async () => {
758
+ let cancelled = 0;
759
+ await assertCancelledAfterClose(new Response(new ReadableStream({ cancel() {
760
+ cancelled++;
761
+ return new Promise(() => {});
762
+ } })));
763
+ deepStrictEqual(cancelled, 1);
764
+ });
765
+ await t.step("failing cancel", async () => {
766
+ const error = /* @__PURE__ */ new Error("cancel");
767
+ const body = createTrackedBody({ cancelError: error });
768
+ const { records } = await captureLogs(async () => {
769
+ await assertCancelledAfterClose(new Response(body.stream));
770
+ await settleCancellations();
771
+ });
772
+ deepStrictEqual(body.state.cancelled, 1);
773
+ deepStrictEqual(cancelFailureLogs(records), [[
774
+ "debug",
775
+ url,
776
+ error
777
+ ]]);
778
+ });
779
+ });
780
+ test("withPublicFetch()", async (t) => {
781
+ const url = "https://pinned-host.invalid/";
782
+ function setUp() {
783
+ const bodies = [];
784
+ const { lookup } = createRecordingLookup(publicAddresses);
785
+ const recorder = createRecordingPinner(() => {
786
+ const body = createTrackedBody();
787
+ bodies.push(body);
788
+ return new Response(body.stream);
789
+ });
790
+ const { withPublicFetch } = createPublicFetch({
791
+ pinner: () => Promise.resolve(recorder.pinner),
792
+ lookup: failingLookup
793
+ });
794
+ return {
795
+ bodies,
796
+ recorder,
797
+ lookup,
798
+ withPublicFetch
799
+ };
800
+ }
801
+ await t.step("returns the result", async () => {
802
+ const { bodies, recorder, lookup, withPublicFetch } = setUp();
803
+ deepStrictEqual(await withPublicFetch(url, async (fetch) => await (await fetch(url)).text(), { lookup }), "body");
804
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [0]);
805
+ deepStrictEqual(recorder.released.length, 1);
806
+ });
807
+ await t.step("closes the session when the callback throws", async () => {
808
+ const { bodies, recorder, lookup, withPublicFetch } = setUp();
809
+ const error = /* @__PURE__ */ new Error("callback");
810
+ let fetchInScope;
811
+ await rejects(() => withPublicFetch(url, async (fetch) => {
812
+ fetchInScope = fetch;
813
+ await fetch(url);
814
+ throw error;
815
+ }, { lookup }), (e) => e === error);
816
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [1]);
817
+ deepStrictEqual(recorder.released.length, 1);
818
+ await rejects(() => fetchInScope(url), TypeError);
819
+ });
820
+ await t.step("rejects returning a Response", async () => {
821
+ const { bodies, recorder, lookup, withPublicFetch } = setUp();
822
+ await rejects(() => withPublicFetch(url, (fetch) => fetch(url), { lookup }), TypeError);
823
+ deepStrictEqual(bodies.map(({ state }) => state.cancelled), [1]);
824
+ deepStrictEqual(recorder.released.length, 1);
825
+ });
826
+ await t.step("does not call the callback for disallowed URLs", async () => {
827
+ const { recorder, withPublicFetch } = setUp();
828
+ let called = false;
829
+ await rejects(() => withPublicFetch(url, () => {
830
+ called = true;
831
+ return Promise.resolve();
832
+ }, { lookup: () => Promise.resolve([{
833
+ address: "10.0.0.1",
834
+ family: 4
835
+ }]) }), UrlError);
836
+ ok(!called);
837
+ deepStrictEqual(recorder.calls, []);
838
+ deepStrictEqual(recorder.released, []);
839
+ });
840
+ });
841
+ test("openPublicFetch() honors the abort signal", async () => {
842
+ const recorder = createRecordingPinner();
843
+ const { openPublicFetch } = createPublicFetch({
844
+ pinner: () => Promise.resolve(recorder.pinner),
845
+ lookup: failingLookup
846
+ });
847
+ const url = "https://pinned-host.invalid/";
848
+ const aborted = new AbortController();
849
+ aborted.abort(/* @__PURE__ */ new Error("aborted"));
850
+ const before = createRecordingLookup(publicAddresses);
851
+ await rejects(() => openPublicFetch(url, {
852
+ signal: aborted.signal,
853
+ ...before
854
+ }), (e) => e === aborted.signal.reason);
855
+ deepStrictEqual(before.hostnames, []);
856
+ const during = new AbortController();
857
+ await rejects(() => openPublicFetch(url, {
858
+ signal: during.signal,
859
+ lookup() {
860
+ during.abort(/* @__PURE__ */ new Error("aborted during lookup"));
861
+ return Promise.resolve(publicAddresses);
862
+ }
863
+ }), (e) => e === during.signal.reason);
864
+ deepStrictEqual(recorder.calls, []);
865
+ deepStrictEqual(recorder.released, []);
866
+ });
867
+ function withNavigatorUserAgent(userAgent, fn) {
868
+ const descriptor = Object.getOwnPropertyDescriptor(navigator, "userAgent");
869
+ Object.defineProperty(navigator, "userAgent", {
870
+ value: userAgent,
871
+ configurable: true
872
+ });
873
+ const restore = () => {
874
+ if (descriptor == null) delete navigator.userAgent;
875
+ else Object.defineProperty(navigator, "userAgent", descriptor);
876
+ };
877
+ return fn().finally(restore);
878
+ }
879
+ function isUnpinnedLog(record) {
880
+ return typeof record.rawMessage === "string" && record.rawMessage.startsWith("Outbound {protocol} connections are not");
881
+ }
882
+ test("openPublicFetch() uses the global fetch() without a pinner", async () => {
883
+ const { lookup, hostnames } = createRecordingLookup(publicAddresses);
884
+ const unsupported = createRecordingPinner();
885
+ unsupported.pinner.supports = () => false;
886
+ const pinners = [null, unsupported.pinner];
887
+ const { openPublicFetch } = createPublicFetch({
888
+ pinner: () => Promise.resolve(pinners[hostnames.length % 2]),
889
+ lookup: failingLookup
890
+ });
891
+ const { records } = await captureLogs(() => withNavigatorUserAgent("Cloudflare-Workers", () => withMockedGlobalFetch(async () => {
892
+ esm_default.route("begin:https://unpinned-host.invalid/", "https");
893
+ esm_default.route("begin:http://unpinned-host.invalid/", "http");
894
+ for (const protocol of [
895
+ "https",
896
+ "http",
897
+ "https",
898
+ "http",
899
+ "https"
900
+ ]) {
901
+ const url = `${protocol}://unpinned-host.invalid/${hostnames.length}`;
902
+ const session = await openPublicFetch(url, { lookup });
903
+ try {
904
+ deepStrictEqual(await (await session.fetch(url)).text(), protocol);
905
+ } finally {
906
+ await session.close();
907
+ }
908
+ }
909
+ deepStrictEqual(esm_default.callHistory.calls().map(({ url }) => url), [
910
+ "https://unpinned-host.invalid/0",
911
+ "http://unpinned-host.invalid/1",
912
+ "https://unpinned-host.invalid/2",
913
+ "http://unpinned-host.invalid/3",
914
+ "https://unpinned-host.invalid/4"
915
+ ]);
916
+ await rejects(() => openPublicFetch("https://unpinned-host.invalid/", { lookup: () => Promise.resolve([{
917
+ address: "127.0.0.1",
918
+ family: 4
919
+ }]) }), UrlError);
920
+ deepStrictEqual(esm_default.callHistory.calls().length, 5);
921
+ })));
922
+ deepStrictEqual(unsupported.calls, []);
923
+ deepStrictEqual(unsupported.released, []);
924
+ deepStrictEqual(records.filter(isUnpinnedLog).map((r) => [
925
+ r.level,
926
+ r.properties.runtime,
927
+ r.properties.protocol
928
+ ]), [[
929
+ "debug",
930
+ "cfworkers",
931
+ "https:"
932
+ ], [
933
+ "debug",
934
+ "cfworkers",
935
+ "http:"
936
+ ]]);
937
+ });
938
+ test("openPublicFetch() uses the global fetch() on Deno", { ignore: !("Deno" in globalThis) }, async () => {
939
+ const { lookup, hostnames } = createRecordingLookup(publicAddresses);
940
+ await withMockedGlobalFetch(async () => {
941
+ esm_default.route("https://deno-host.invalid/object", { body: "ok" });
942
+ for (let i = 0; i < 2; i++) {
943
+ const session = await openPublicFetch("https://deno-host.invalid/object", { lookup });
944
+ try {
945
+ deepStrictEqual(await (await session.fetch("https://deno-host.invalid/object", { redirect: "follow" })).text(), "ok");
946
+ } finally {
947
+ await session.close();
948
+ }
949
+ }
950
+ deepStrictEqual(esm_default.callHistory.calls().map(({ url, args }) => [url, args[1].redirect]), [["https://deno-host.invalid/object", "manual"], ["https://deno-host.invalid/object", "manual"]]);
951
+ });
952
+ deepStrictEqual(hostnames, ["deno-host.invalid", "deno-host.invalid"]);
953
+ const workerUrl = new URL("./testdata/public-fetch-deno-worker.ts", import.meta.url);
954
+ if ((await Deno.permissions.query({
955
+ name: "read",
956
+ path: workerUrl
957
+ })).state !== "granted") return;
958
+ const worker = new Worker(workerUrl, { type: "module" });
959
+ try {
960
+ deepStrictEqual(await settlesWithin(new Promise((resolve, reject) => {
961
+ worker.onmessage = (event) => resolve(event.data);
962
+ worker.onerror = (event) => {
963
+ event.preventDefault();
964
+ reject(new Error(event.message));
965
+ };
966
+ }), 1e4), {
967
+ calls: [
968
+ ["https://deno-host.invalid/1", "manual"],
969
+ ["https://deno-host.invalid/2", "manual"],
970
+ ["http://deno-host.invalid/3", "manual"]
971
+ ],
972
+ hostnames: [
973
+ "deno-host.invalid",
974
+ "deno-host.invalid",
975
+ "deno-host.invalid"
976
+ ],
977
+ bodies: [
978
+ "ok",
979
+ "ok",
980
+ "ok"
981
+ ],
982
+ logs: [[
983
+ "debug",
984
+ "deno",
985
+ "https:"
986
+ ], [
987
+ "debug",
988
+ "deno",
989
+ "http:"
990
+ ]]
991
+ });
992
+ } finally {
993
+ worker.terminate();
994
+ }
995
+ });
996
+ test("openPublicFetch() pins connections on Node.js", { ignore: detectRuntime() !== "node" }, async () => {
997
+ const { lookup, hostnames } = createRecordingLookup(publicAddresses);
998
+ await withMockedGlobalFetch(async () => {
999
+ esm_default.route("https://node-host.invalid/object", { body: "ok" });
1000
+ const session = await openPublicFetch("https://node-host.invalid/object", { lookup });
1001
+ try {
1002
+ deepStrictEqual(await (await session.fetch("https://node-host.invalid/object", { redirect: "follow" })).text(), "ok");
1003
+ } finally {
1004
+ await session.close();
1005
+ }
1006
+ const calls = esm_default.callHistory.calls();
1007
+ deepStrictEqual(calls.length, 1);
1008
+ const init = calls[0].args[1];
1009
+ deepStrictEqual(calls[0].url, "https://node-host.invalid/object");
1010
+ deepStrictEqual(init.redirect, "manual");
1011
+ ok(typeof init.dispatcher?.dispatch === "function", String(init.dispatcher));
1012
+ });
1013
+ deepStrictEqual(hostnames, ["node-host.invalid"]);
1014
+ });
1015
+ test("openPublicFetch() rejects fedify-test.internal", async (t) => {
1016
+ const url = "https://fedify-test.internal/";
1017
+ const cases = [{
1018
+ name: "private address",
1019
+ lookup: () => Promise.resolve([{
1020
+ address: "10.0.0.1",
1021
+ family: 4
1022
+ }]),
1023
+ reason: "disallowed"
1024
+ }, {
1025
+ name: "lookup failure",
1026
+ lookup: () => Promise.reject(Object.assign(/* @__PURE__ */ new Error("getaddrinfo ENOTFOUND"), { code: "ENOTFOUND" })),
1027
+ reason: "dns"
1028
+ }];
1029
+ for (const { name, lookup, reason } of cases) await t.step(name, async () => {
1030
+ const recorder = createRecordingPinner();
1031
+ const recorded = createPublicFetch({
1032
+ pinner: () => Promise.resolve(recorder.pinner),
1033
+ lookup: failingLookup
1034
+ });
1035
+ await withMockedGlobalFetch(async () => {
1036
+ esm_default.catch(200);
1037
+ for (const open of [openPublicFetch, recorded.openPublicFetch]) await rejects(() => open(url, { lookup }), (error) => {
1038
+ ok(error instanceof UrlError, String(error));
1039
+ deepStrictEqual(error.reason, reason);
1040
+ return true;
1041
+ });
1042
+ deepStrictEqual(esm_default.callHistory.calls(), []);
1043
+ });
1044
+ deepStrictEqual(recorder.calls, []);
1045
+ deepStrictEqual(recorder.released, []);
1046
+ });
1047
+ });
1048
+ test("openDirectFetch() follows the session rules", async () => {
1049
+ let hits = 0;
1050
+ const server = createServer((_, res) => {
1051
+ hits++;
1052
+ res.writeHead(200, { "Content-Type": "text/plain" });
1053
+ res.write("partial");
1054
+ });
1055
+ const port = await listen(server);
1056
+ try {
1057
+ const url = `http://127.0.0.1:${port}/`;
1058
+ const session = await openDirectFetch(url);
1059
+ deepStrictEqual(session.url.href, url);
1060
+ await rejects(() => session.fetch(`http://127.0.0.1:${port + 1}/`), TypeError);
1061
+ const response = await session.fetch(`${url}path`);
1062
+ deepStrictEqual(response.status, 200);
1063
+ ok(!response.bodyUsed);
1064
+ await session.close();
1065
+ ok(response.bodyUsed);
1066
+ await rejects(() => session.fetch(url), TypeError);
1067
+ await session.close();
1068
+ deepStrictEqual(hits, 1);
1069
+ } finally {
1070
+ await closeServer(server);
1071
+ }
1072
+ });
1073
+ //#endregion
1074
+ export {};