@fedify/vocab-runtime 2.4.0-pr.934.40 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/deno.json +4 -2
  2. package/dist/{tests/docloader-Xi61Le8w.mjs → contexts-BK1CqBR5.cjs} +164 -269
  3. package/dist/{tests/docloader-BFedvemb.cjs → contexts-DPuJ4UYL.js} +160 -288
  4. package/dist/{docloader-DnUMWHaJ.d.cts → docloader-CYwqh5Df.d.cts} +68 -2
  5. package/dist/{docloader-xRGn1azD.d.ts → docloader-CYwqh5Df.d.ts} +68 -2
  6. package/dist/internal/jsonld-cache.cjs +70 -1
  7. package/dist/internal/jsonld-cache.d.cts +35 -2
  8. package/dist/internal/jsonld-cache.d.ts +35 -2
  9. package/dist/internal/jsonld-cache.js +67 -2
  10. package/dist/internal/portable-dereference.cjs +721 -0
  11. package/dist/internal/portable-dereference.d.cts +349 -0
  12. package/dist/internal/portable-dereference.d.ts +349 -0
  13. package/dist/internal/portable-dereference.js +702 -0
  14. package/dist/internal/signed-representation.cjs +378 -0
  15. package/dist/internal/signed-representation.d.cts +144 -0
  16. package/dist/internal/signed-representation.d.ts +144 -0
  17. package/dist/internal/signed-representation.js +371 -0
  18. package/dist/jsonld.cjs +2 -2
  19. package/dist/mod.cjs +715 -4523
  20. package/dist/mod.d.cts +406 -6
  21. package/dist/mod.d.ts +406 -7
  22. package/dist/mod.js +694 -4518
  23. package/dist/portable-DqtfLy_1.d.cts +160 -0
  24. package/dist/portable-DtWsu2yU.d.ts +160 -0
  25. package/dist/tests/body-CYiu9CO-.mjs +130 -0
  26. package/dist/tests/body-CkEwROsn.cjs +153 -0
  27. package/dist/tests/body.test.cjs +89 -0
  28. package/dist/tests/body.test.d.cts +1 -0
  29. package/dist/tests/body.test.d.mts +1 -0
  30. package/dist/tests/body.test.mjs +90 -0
  31. package/dist/tests/contexts-CIKsin4e.mjs +4512 -0
  32. package/dist/tests/contexts-DizzBjz4.cjs +4523 -0
  33. package/dist/tests/decimal.test.cjs +8 -7
  34. package/dist/tests/decimal.test.mjs +8 -6
  35. package/dist/tests/digest-3FeH2Y-Q.cjs +176 -0
  36. package/dist/tests/digest-COC7xDiQ.mjs +141 -0
  37. package/dist/tests/digest.test.cjs +102 -0
  38. package/dist/tests/digest.test.d.cts +1 -0
  39. package/dist/tests/digest.test.d.mts +1 -0
  40. package/dist/tests/digest.test.mjs +103 -0
  41. package/dist/tests/docloader-C3YFl6y4.cjs +396 -0
  42. package/dist/tests/docloader-CvmS4sVq.mjs +373 -0
  43. package/dist/tests/docloader.test.cjs +692 -39
  44. package/dist/tests/docloader.test.mjs +686 -34
  45. package/dist/tests/internal/multicodec.test.cjs +2 -3
  46. package/dist/tests/internal/multicodec.test.mjs +2 -2
  47. package/dist/tests/internal/portable-dereference.test.cjs +172 -0
  48. package/dist/tests/internal/portable-dereference.test.d.cts +1 -0
  49. package/dist/tests/internal/portable-dereference.test.d.mts +1 -0
  50. package/dist/tests/internal/portable-dereference.test.mjs +173 -0
  51. package/dist/tests/jsonld-cache-BPQmOZWD.mjs +342 -0
  52. package/dist/tests/jsonld-cache-C07AyNOY.cjs +397 -0
  53. package/dist/tests/jsonld-cache.test.cjs +101 -298
  54. package/dist/tests/jsonld-cache.test.mjs +94 -289
  55. package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
  56. package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
  57. package/dist/tests/key.test.cjs +6 -5
  58. package/dist/tests/key.test.mjs +6 -4
  59. package/dist/tests/langstr.test.cjs +4 -4
  60. package/dist/tests/langstr.test.mjs +2 -2
  61. package/dist/tests/link.test.cjs +2 -3
  62. package/dist/tests/link.test.mjs +2 -2
  63. package/dist/tests/multibase/multibase.test.cjs +8 -9
  64. package/dist/tests/multibase/multibase.test.mjs +6 -6
  65. package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
  66. package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
  67. package/dist/tests/portable-dereference-BNXtgg5U.cjs +278 -0
  68. package/dist/tests/portable-dereference-DRE5bz-l.mjs +249 -0
  69. package/dist/tests/portable-media-BLKllU56.cjs +153 -0
  70. package/dist/tests/portable-media-DRSGBORB.mjs +148 -0
  71. package/dist/tests/portable-media.test.cjs +222 -0
  72. package/dist/tests/portable-media.test.d.cts +1 -0
  73. package/dist/tests/portable-media.test.d.mts +1 -0
  74. package/dist/tests/portable-media.test.mjs +223 -0
  75. package/dist/tests/portable-workers.test.cjs +36 -0
  76. package/dist/tests/portable-workers.test.d.cts +2 -0
  77. package/dist/tests/portable-workers.test.d.mts +2 -0
  78. package/dist/tests/portable-workers.test.mjs +35 -0
  79. package/dist/tests/{request-cCPgOxYG.cjs → request-8vPWtV1-.cjs} +10 -4
  80. package/dist/tests/{request-BOS-hNaf.mjs → request-DRaOaTqD.mjs} +8 -2
  81. package/dist/tests/request.test.cjs +7 -4
  82. package/dist/tests/request.test.mjs +5 -2
  83. package/dist/tests/signed-representation.test.cjs +588 -0
  84. package/dist/tests/signed-representation.test.d.cts +1 -0
  85. package/dist/tests/signed-representation.test.d.mts +1 -0
  86. package/dist/tests/signed-representation.test.mjs +589 -0
  87. package/dist/tests/temporal.test.cjs +1 -2
  88. package/dist/tests/temporal.test.mjs +1 -1
  89. package/dist/tests/url-BNakuZ8k.cjs +998 -0
  90. package/dist/tests/url-DMxmp7ZG.mjs +859 -0
  91. package/dist/tests/url.test.cjs +503 -3
  92. package/dist/tests/url.test.mjs +503 -2
  93. package/dist/url-DrGTR8yv.cjs +993 -0
  94. package/dist/url-Dzyp-NsC.js +860 -0
  95. package/package.json +27 -4
  96. package/scripts/test-bun.mjs +17 -0
  97. package/src/body.test.ts +125 -0
  98. package/src/body.ts +152 -0
  99. package/src/contexts/cid-v1.json +114 -0
  100. package/src/contexts/fep-22cd.json +21 -0
  101. package/src/contexts/fep-ef61.json +10 -0
  102. package/src/contexts/miscellany.json +17 -0
  103. package/src/contexts/security-data-integrity-v1.json +0 -4
  104. package/src/contexts.ts +40 -0
  105. package/src/digest.test.ts +220 -0
  106. package/src/digest.ts +229 -0
  107. package/src/docloader.test.ts +918 -27
  108. package/src/docloader.ts +322 -105
  109. package/src/internal/jsonld-cache.ts +97 -1
  110. package/src/internal/portable-dereference.test.ts +254 -0
  111. package/src/internal/portable-dereference.ts +1000 -0
  112. package/src/internal/signed-representation.ts +565 -0
  113. package/src/jsonld-cache.test.ts +89 -0
  114. package/src/key.test.ts +9 -0
  115. package/src/key.ts +11 -1
  116. package/src/mod.ts +30 -0
  117. package/src/multibase/multibase.test.ts +5 -5
  118. package/src/portable-media.test.ts +293 -0
  119. package/src/portable-media.ts +264 -0
  120. package/src/portable-workers.test.ts +81 -0
  121. package/src/portable.ts +180 -0
  122. package/src/preprocessor.ts +7 -0
  123. package/src/request.test.ts +10 -1
  124. package/src/request.ts +9 -1
  125. package/src/signed-representation.test.ts +338 -0
  126. package/src/url.test.ts +844 -1
  127. package/src/url.ts +717 -17
  128. package/tsdown.config.ts +2 -0
  129. package/dist/tests/url-BvjYQdxL.cjs +0 -456
  130. package/dist/tests/url-a2D8NAgh.mjs +0 -378
  131. package/dist/url-Ck3dGEwH.cjs +0 -457
  132. package/dist/url-m1YxGNZ0.js +0 -379
  133. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  134. /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-C4Fl80ae.cjs} +0 -0
  135. /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-CQ26J_L7.mjs} +0 -0
  136. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  137. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  138. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  139. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  140. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -1,8 +1,13 @@
1
- import { n as getRemoteDocument, r as preloadedContexts, t as getDocumentLoader } from "./docloader-Xi61Le8w.mjs";
2
- import { t as FetchError } from "./request-BOS-hNaf.mjs";
3
- import { t as UrlError } from "./url-a2D8NAgh.mjs";
4
- import { deepStrictEqual, ok, rejects } from "node:assert";
5
- import { test } from "node:test";
1
+ import { t as FetchError } from "./request-DRaOaTqD.mjs";
2
+ import { n as cid_v1_default, t as preloadedContexts } from "./contexts-CIKsin4e.mjs";
3
+ import { n as UrlError } from "./url-DMxmp7ZG.mjs";
4
+ import { i as withDocumentLoaderTimeout, n as getRemoteDocument, r as resolveDocumentLoaderTimeout, t as getDocumentLoader } from "./docloader-CvmS4sVq.mjs";
5
+ import { gzipSync } from "node:zlib";
6
+ import { configure, reset } from "@logtape/logtape";
7
+ import { deepStrictEqual, ok, rejects, throws } from "node:assert";
8
+ import dns from "node:dns/promises";
9
+ import { test } from "@fedify/fixture";
10
+ import { createServer } from "node:http";
6
11
  //#region \0rolldown/runtime.js
7
12
  var __create = Object.create;
8
13
  var __defProp = Object.defineProperty;
@@ -130,27 +135,27 @@ function parse(input, loose) {
130
135
  }
131
136
  //#endregion
132
137
  //#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/TypeDescriptor.js
133
- const valueTypes = new Set([
138
+ const valueTypes = /* @__PURE__ */ new Set([
134
139
  "boolean",
135
140
  "number",
136
141
  "null",
137
142
  "string",
138
143
  "undefined"
139
144
  ]);
140
- const referenceTypes = new Set([
145
+ const referenceTypes = /* @__PURE__ */ new Set([
141
146
  "array",
142
147
  "function",
143
148
  "object",
144
149
  "symbol"
145
150
  ]);
146
- const detectableTypes = new Set([
151
+ const detectableTypes = /* @__PURE__ */ new Set([
147
152
  "boolean",
148
153
  "function",
149
154
  "number",
150
155
  "string",
151
156
  "symbol"
152
157
  ]);
153
- const typeConstructors = new Set([
158
+ const typeConstructors = /* @__PURE__ */ new Set([
154
159
  Boolean,
155
160
  Number,
156
161
  String
@@ -221,7 +226,7 @@ var TypeDescriptor = class TypeDescriptor {
221
226
  };
222
227
  //#endregion
223
228
  //#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/IsSubsetOf.js
224
- const allowedTypes = new Set([
229
+ const allowedTypes = /* @__PURE__ */ new Set([
225
230
  "array",
226
231
  "object",
227
232
  "function",
@@ -713,7 +718,14 @@ var __classPrivateFieldGet = function(receiver, state, kind, f) {
713
718
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
714
719
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
715
720
  };
716
- var _Route_instances, _a, _Route_responseSubscriptions, _Route_validate, _Route_sanitize, _Route_generateMatcher, _Route_limit, _Route_delayResponse;
721
+ var _Route_instances;
722
+ var _a;
723
+ var _Route_responseSubscriptions;
724
+ var _Route_validate;
725
+ var _Route_sanitize;
726
+ var _Route_generateMatcher;
727
+ var _Route_limit;
728
+ var _Route_delayResponse;
717
729
  var RouteConfigWrapper = class {
718
730
  constructor(config) {
719
731
  Object.assign(this, config);
@@ -1225,6 +1237,52 @@ var esm_default = new class FetchMock {
1225
1237
  }({ ...defaultFetchMockConfig });
1226
1238
  //#endregion
1227
1239
  //#region src/docloader.test.ts
1240
+ test("getRemoteDocument() forwards options to alternate documents", async () => {
1241
+ const records = [];
1242
+ await reset();
1243
+ await configure({
1244
+ sinks: { capture: (record) => records.push(record) },
1245
+ loggers: [{
1246
+ category: ["fedify"],
1247
+ lowestLevel: "debug",
1248
+ sinks: ["capture"]
1249
+ }]
1250
+ });
1251
+ try {
1252
+ for (const linkHeader of [true, false]) for (const suppressError of [
1253
+ true,
1254
+ false,
1255
+ void 0
1256
+ ]) {
1257
+ records.length = 0;
1258
+ const url = "https://example.com/alternate-source";
1259
+ const alternateUrl = "https://example.com/missing-alternate";
1260
+ const options = {
1261
+ suppressError,
1262
+ signal: new AbortController().signal
1263
+ };
1264
+ const response = new Response(`<link rel="alternate" type="application/activity+json" href="${alternateUrl}">`, { headers: {
1265
+ "Content-Type": "text/html",
1266
+ ...linkHeader ? { Link: `<${alternateUrl}>; rel="alternate"; type="application/activity+json"` } : {}
1267
+ } });
1268
+ let alternateRequests = 0;
1269
+ await rejects(() => getRemoteDocument(url, response, async (requestedUrl, forwarded) => {
1270
+ alternateRequests++;
1271
+ deepStrictEqual(requestedUrl, alternateUrl);
1272
+ deepStrictEqual(forwarded, options);
1273
+ return await getRemoteDocument(requestedUrl, new Response(null, { status: 404 }), () => {
1274
+ throw new Error("Unexpected additional alternate");
1275
+ }, forwarded);
1276
+ }, options), FetchError);
1277
+ deepStrictEqual(alternateRequests, 1);
1278
+ const failures = records.filter((record) => record.rawMessage === "Failed to fetch document: {status} {url} {headers}");
1279
+ deepStrictEqual(failures.length, 1);
1280
+ deepStrictEqual(failures[0].level, suppressError ? "warning" : "error");
1281
+ }
1282
+ } finally {
1283
+ await reset();
1284
+ }
1285
+ });
1228
1286
  test("new FetchError()", () => {
1229
1287
  const e = new FetchError("https://example.com/", "An error message.");
1230
1288
  deepStrictEqual(e.name, "FetchError");
@@ -1247,7 +1305,7 @@ test("getDocumentLoader()", async (t) => {
1247
1305
  name: "Fetched object",
1248
1306
  type: "Object"
1249
1307
  } });
1250
- await t.test("ok", async () => {
1308
+ await t.step("ok", async () => {
1251
1309
  deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
1252
1310
  contextUrl: null,
1253
1311
  documentUrl: "https://example.com/object",
@@ -1290,7 +1348,7 @@ test("getDocumentLoader()", async (t) => {
1290
1348
  Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
1291
1349
  }
1292
1350
  });
1293
- await t.test("Link header", async () => {
1351
+ await t.step("Link header", async () => {
1294
1352
  deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
1295
1353
  contextUrl: "https://www.w3.org/ns/activitystreams",
1296
1354
  documentUrl: "https://example.com/link-ctx",
@@ -1311,7 +1369,7 @@ test("getDocumentLoader()", async (t) => {
1311
1369
  }
1312
1370
  });
1313
1371
  });
1314
- await t.test("Link header relative url", async () => {
1372
+ await t.step("Link header relative url", async () => {
1315
1373
  deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
1316
1374
  contextUrl: "https://www.w3.org/ns/activitystreams",
1317
1375
  documentUrl: "https://example.com/link-ctx",
@@ -1332,7 +1390,7 @@ test("getDocumentLoader()", async (t) => {
1332
1390
  }
1333
1391
  });
1334
1392
  });
1335
- await t.test("wrong Link header syntax", async () => {
1393
+ await t.step("wrong Link header syntax", async () => {
1336
1394
  deepStrictEqual(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
1337
1395
  contextUrl: null,
1338
1396
  documentUrl: "https://example.com/obj-w-wrong-link",
@@ -1356,7 +1414,7 @@ test("getDocumentLoader()", async (t) => {
1356
1414
  </html>`,
1357
1415
  headers: { "Content-Type": "text/html; charset=utf-8" }
1358
1416
  });
1359
- await t.test("HTML <link>", async () => {
1417
+ await t.step("HTML <link>", async () => {
1360
1418
  deepStrictEqual(await fetchDocumentLoader("https://example.com/html-link"), {
1361
1419
  contextUrl: null,
1362
1420
  documentUrl: "https://example.com/object",
@@ -1380,7 +1438,7 @@ test("getDocumentLoader()", async (t) => {
1380
1438
  </html>`,
1381
1439
  headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" }
1382
1440
  });
1383
- await t.test("XHTML <link>", async () => {
1441
+ await t.step("XHTML <link>", async () => {
1384
1442
  deepStrictEqual(await fetchDocumentLoader("https://example.com/xhtml-link"), {
1385
1443
  contextUrl: null,
1386
1444
  documentUrl: "https://example.com/object",
@@ -1406,7 +1464,7 @@ test("getDocumentLoader()", async (t) => {
1406
1464
  </html>`,
1407
1465
  headers: { "Content-Type": "text/html; charset=utf-8" }
1408
1466
  });
1409
- await t.test("HTML <a>", async () => {
1467
+ await t.step("HTML <a>", async () => {
1410
1468
  deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
1411
1469
  contextUrl: null,
1412
1470
  documentUrl: "https://example.com/object",
@@ -1428,7 +1486,7 @@ test("getDocumentLoader()", async (t) => {
1428
1486
  </html>`,
1429
1487
  headers: { "Content-Type": "text/html; charset=utf-8" }
1430
1488
  });
1431
- await t.test("HTML without ActivityPub alternate link", async () => {
1489
+ await t.step("HTML without ActivityPub alternate link", async () => {
1432
1490
  await rejects(() => fetchDocumentLoader("https://example.com/html-no-alternate"), (error) => {
1433
1491
  ok(error instanceof FetchError);
1434
1492
  ok(error.message.includes("HTML document has no ActivityPub alternate link"));
@@ -1448,7 +1506,7 @@ test("getDocumentLoader()", async (t) => {
1448
1506
  },
1449
1507
  headers: { "Content-Type": "text/html; charset=utf-8" }
1450
1508
  });
1451
- await t.test("wrong Content-Type with JSON body", async () => {
1509
+ await t.step("wrong Content-Type with JSON body", async () => {
1452
1510
  deepStrictEqual(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
1453
1511
  contextUrl: null,
1454
1512
  documentUrl: "https://example.com/wrong-content-type",
@@ -1467,7 +1525,7 @@ test("getDocumentLoader()", async (t) => {
1467
1525
  "Content-Type": "text/html; charset=utf-8"
1468
1526
  }
1469
1527
  });
1470
- await t.test("HTML Content-Length over limit", async () => {
1528
+ await t.step("HTML Content-Length over limit", async () => {
1471
1529
  await rejects(() => fetchDocumentLoader("https://example.com/large-html"), (error) => {
1472
1530
  ok(error instanceof FetchError);
1473
1531
  ok(error.message.includes("HTML document is too large to scan for an ActivityPub alternate link"));
@@ -1477,32 +1535,88 @@ test("getDocumentLoader()", async (t) => {
1477
1535
  return true;
1478
1536
  });
1479
1537
  });
1480
- await t.test("HTML Content-Length over limit cancels body", async () => {
1538
+ await t.step("HTML Content-Length over limit cancels body", async () => {
1481
1539
  let canceled = false;
1482
1540
  const response = new Response("<!DOCTYPE html>", { headers: {
1483
1541
  "Content-Length": String(1048577),
1484
1542
  "Content-Type": "text/html; charset=utf-8"
1485
1543
  } });
1486
- Object.defineProperty(response, "body", { value: { cancel: () => {
1544
+ Object.defineProperty(response, "body", { value: new ReadableStream({ cancel() {
1487
1545
  canceled = true;
1488
- } } });
1546
+ } }) });
1489
1547
  await rejects(() => getRemoteDocument("https://example.com/large-html-cancel", response, () => {
1490
1548
  throw new Error("unexpected alternate fetch");
1491
1549
  }), FetchError);
1492
1550
  deepStrictEqual(canceled, true);
1493
1551
  });
1494
1552
  esm_default.get("https://example.com/404", { status: 404 });
1495
- await t.test("not ok", async () => {
1553
+ await t.step("not ok", async () => {
1496
1554
  await rejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
1497
1555
  });
1498
- await t.test("preloaded contexts", async () => {
1556
+ await t.step("preloaded contexts", async () => {
1499
1557
  for (const [url, document] of Object.entries(preloadedContexts)) deepStrictEqual(await fetchDocumentLoader(url), {
1500
1558
  contextUrl: null,
1501
1559
  documentUrl: url,
1502
1560
  document
1503
1561
  });
1504
1562
  });
1505
- await t.test("deny non-HTTP/HTTPS", async () => {
1563
+ esm_default.get("https://www.w3.org/ns/cid/v1", { status: 503 });
1564
+ await t.step("preloaded CID v1 context", async () => {
1565
+ const url = "https://www.w3.org/ns/cid/v1";
1566
+ deepStrictEqual(await fetchDocumentLoader(url), {
1567
+ contextUrl: null,
1568
+ documentUrl: url,
1569
+ document: cid_v1_default
1570
+ });
1571
+ });
1572
+ await t.step("preloaded FEP-ef61 context", async () => {
1573
+ const url = "https://w3id.org/fep/ef61";
1574
+ ok(url in preloadedContexts);
1575
+ deepStrictEqual(await fetchDocumentLoader(url), {
1576
+ contextUrl: null,
1577
+ documentUrl: url,
1578
+ document: { "@context": {
1579
+ gateways: {
1580
+ "@id": "https://w3id.org/fep/ef61/gateways",
1581
+ "@type": "@id",
1582
+ "@container": "@list"
1583
+ },
1584
+ digestMultibase: "https://www.w3.org/ns/credentials/v2#digestMultibase"
1585
+ } }
1586
+ });
1587
+ });
1588
+ esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
1589
+ await t.step("preloaded FEP-7aa9 context", async () => {
1590
+ const url = "https://w3id.org/fep/7aa9";
1591
+ deepStrictEqual(await fetchDocumentLoader(url), {
1592
+ contextUrl: null,
1593
+ documentUrl: url,
1594
+ document: { "@context": {
1595
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
1596
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
1597
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
1598
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
1599
+ "topic": {
1600
+ "@id": "https://w3id.org/fep/7aa9#topic",
1601
+ "@type": "@id"
1602
+ },
1603
+ "featuredObject": {
1604
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
1605
+ "@type": "@id"
1606
+ },
1607
+ "canFeature": {
1608
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
1609
+ "@type": "@id"
1610
+ },
1611
+ "featureAuthorization": {
1612
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
1613
+ "@type": "@id"
1614
+ }
1615
+ } }
1616
+ });
1617
+ deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
1618
+ });
1619
+ await t.step("deny non-HTTP/HTTPS", async () => {
1506
1620
  await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
1507
1621
  });
1508
1622
  esm_default.get("https://example.com/localhost-redirect", {
@@ -1527,13 +1641,13 @@ test("getDocumentLoader()", async (t) => {
1527
1641
  name: "Fetched object",
1528
1642
  type: "Object"
1529
1643
  } });
1530
- await t.test("allowPrivateAddress: false", async () => {
1644
+ await t.step("allowPrivateAddress: false", async () => {
1531
1645
  await rejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
1532
1646
  await rejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
1533
1647
  await rejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
1534
1648
  });
1535
1649
  const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
1536
- await t.test("allowPrivateAddress: true", async () => {
1650
+ await t.step("allowPrivateAddress: true", async () => {
1537
1651
  const expected = {
1538
1652
  contextUrl: null,
1539
1653
  documentUrl: "https://localhost/object",
@@ -1556,12 +1670,12 @@ test("getDocumentLoader()", async (t) => {
1556
1670
  headers: { Location: `https://example.com/too-many-redirects/${Number(cl.url.split("/").at(-1)) + 1}` }
1557
1671
  };
1558
1672
  });
1559
- await t.test("too many redirects", async () => {
1673
+ await t.step("too many redirects", async () => {
1560
1674
  redirectAttempts = 0;
1561
1675
  await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
1562
1676
  deepStrictEqual(redirectAttempts, 21);
1563
1677
  });
1564
- await t.test("custom max redirection", async () => {
1678
+ await t.step("custom max redirection", async () => {
1565
1679
  redirectAttempts = 0;
1566
1680
  const loader = getDocumentLoader({ maxRedirection: 1 });
1567
1681
  await rejects(() => loader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
@@ -1582,7 +1696,7 @@ test("getDocumentLoader()", async (t) => {
1582
1696
  headers: { Location: "https://example.com/redirect-loop-a" }
1583
1697
  };
1584
1698
  });
1585
- await t.test("redirect loop", async () => {
1699
+ await t.step("redirect loop", async () => {
1586
1700
  loopAttempts = 0;
1587
1701
  await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
1588
1702
  deepStrictEqual(loopAttempts, 2);
@@ -1595,7 +1709,7 @@ test("getDocumentLoader()", async (t) => {
1595
1709
  headers: { Location: "/redirect-loop-relative" }
1596
1710
  };
1597
1711
  });
1598
- await t.test("redirect loop with relative location", async () => {
1712
+ await t.step("redirect loop with relative location", async () => {
1599
1713
  relativeLoopAttempts = 0;
1600
1714
  await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
1601
1715
  deepStrictEqual(relativeLoopAttempts, 1);
@@ -1605,7 +1719,7 @@ test("getDocumentLoader()", async (t) => {
1605
1719
  body: maliciousPayload,
1606
1720
  headers: { "Content-Type": "text/html; charset=utf-8" }
1607
1721
  });
1608
- await t.test("ReDoS resistance (CVE-2025-68475)", async () => {
1722
+ await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
1609
1723
  const start = performance.now();
1610
1724
  await rejects(() => fetchDocumentLoader("https://example.com/redos"), FetchError);
1611
1725
  const elapsed = performance.now() - start;
@@ -1613,5 +1727,543 @@ test("getDocumentLoader()", async (t) => {
1613
1727
  });
1614
1728
  esm_default.hardReset();
1615
1729
  });
1730
+ test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
1731
+ const url = "https://example.com/bounded";
1732
+ esm_default.mockGlobal();
1733
+ let oversized = true;
1734
+ try {
1735
+ esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
1736
+ "Content-Type": "application/activity+json",
1737
+ ...oversized ? { "Content-Length": String(16777217) } : {}
1738
+ } }));
1739
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
1740
+ await rejects(loader(url), FetchError);
1741
+ oversized = false;
1742
+ deepStrictEqual((await loader(url)).document, { name: "hello" });
1743
+ oversized = true;
1744
+ esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
1745
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
1746
+ esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
1747
+ "Content-Type": "text/html",
1748
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
1749
+ } }));
1750
+ await rejects(loader(`${url}/alternate`), FetchError);
1751
+ esm_default.get(`${url}/redirect`, {
1752
+ status: 302,
1753
+ headers: { Location: url }
1754
+ });
1755
+ await rejects(loader(`${url}/redirect`), FetchError);
1756
+ } finally {
1757
+ esm_default.hardReset();
1758
+ }
1759
+ });
1760
+ test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
1761
+ const document = { name: "a".repeat(16 * 1024 * 1024) };
1762
+ const compressed = gzipSync(JSON.stringify(document));
1763
+ ok(compressed.byteLength < 64 * 1024);
1764
+ const server = createServer((request, response) => {
1765
+ const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
1766
+ response.writeHead(200, {
1767
+ "Content-Type": "application/activity+json",
1768
+ "Content-Encoding": "gzip",
1769
+ "Content-Length": body.byteLength
1770
+ });
1771
+ response.end(body);
1772
+ });
1773
+ await new Promise((resolve, reject) => {
1774
+ server.once("error", reject);
1775
+ server.listen(0, "127.0.0.1", resolve);
1776
+ });
1777
+ try {
1778
+ const address = server.address();
1779
+ ok(address != null && typeof address !== "string");
1780
+ const url = `http://127.0.0.1:${address.port}/document`;
1781
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
1782
+ deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
1783
+ } finally {
1784
+ await new Promise((resolve, reject) => {
1785
+ server.close((error) => error == null ? resolve() : reject(error));
1786
+ server.closeAllConnections();
1787
+ });
1788
+ }
1789
+ });
1790
+ test("getRemoteDocument() bounds JSON by default", async () => {
1791
+ let canceled = false;
1792
+ let pulls = 0;
1793
+ await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
1794
+ pull(controller) {
1795
+ pulls++;
1796
+ controller.enqueue(new Uint8Array(64 * 1024).fill(32));
1797
+ },
1798
+ cancel() {
1799
+ canceled = true;
1800
+ }
1801
+ }, { highWaterMark: 0 })), () => {
1802
+ throw new Error("Unexpected alternate document");
1803
+ }), FetchError);
1804
+ deepStrictEqual(canceled, true);
1805
+ deepStrictEqual(pulls, 257);
1806
+ });
1807
+ test("getDocumentLoader() logs DNS failures as such", async (t) => {
1808
+ if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
1809
+ const loader = getDocumentLoader();
1810
+ for (const result of [
1811
+ "throws",
1812
+ "empty",
1813
+ "private"
1814
+ ]) await t.step(result, async () => {
1815
+ const originalLookup = dns.lookup;
1816
+ dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1817
+ address: "127.0.0.1",
1818
+ family: 4
1819
+ }]));
1820
+ const records = [];
1821
+ await configure({
1822
+ sinks: { buffer: (record) => records.push(record) },
1823
+ loggers: [{
1824
+ category: "fedify",
1825
+ sinks: ["buffer"],
1826
+ lowestLevel: "debug"
1827
+ }, {
1828
+ category: ["logtape", "meta"],
1829
+ sinks: []
1830
+ }],
1831
+ reset: true
1832
+ });
1833
+ try {
1834
+ const url = "https://dns-failure.invalid/object";
1835
+ let error;
1836
+ await rejects(() => loader(url), (e) => {
1837
+ error = e;
1838
+ return true;
1839
+ });
1840
+ ok(error instanceof UrlError);
1841
+ deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
1842
+ deepStrictEqual(records.map((r) => [
1843
+ r.level,
1844
+ r.rawMessage,
1845
+ r.properties.url
1846
+ ]), [result === "private" ? [
1847
+ "error",
1848
+ "Disallowed private URL: {url}",
1849
+ url
1850
+ ] : [
1851
+ "debug",
1852
+ "DNS lookup failed for {url}",
1853
+ url
1854
+ ]]);
1855
+ ok(records[0].properties.error === error);
1856
+ } finally {
1857
+ await reset();
1858
+ dns.lookup = originalLookup;
1859
+ }
1860
+ });
1861
+ });
1862
+ test("getDocumentLoader() bounds alternate document chains", async (t) => {
1863
+ const base = "https://example.com/alternate-chain/";
1864
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
1865
+ for (const mode of [
1866
+ "header",
1867
+ "html",
1868
+ "mixed"
1869
+ ]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
1870
+ esm_default.mockGlobal();
1871
+ let requests = 0;
1872
+ esm_default.get(`begin:${base}`, ({ url }) => {
1873
+ requests++;
1874
+ const index = Number(new URL(url).pathname.split("/").at(-1));
1875
+ if (index === hops || requests > 30) return Response.json({ done: true });
1876
+ const next = `${base}${index + 1}`;
1877
+ if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
1878
+ return alternate(next, mode === "html");
1879
+ });
1880
+ try {
1881
+ if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
1882
+ else await rejects(loader(`${base}0`), (error) => {
1883
+ ok(error instanceof FetchError);
1884
+ ok(error.message.includes("Too many redirections (21)"));
1885
+ return true;
1886
+ });
1887
+ deepStrictEqual(requests, 21);
1888
+ } finally {
1889
+ esm_default.hardReset();
1890
+ }
1891
+ });
1892
+ for (const mode of [
1893
+ "header",
1894
+ "html",
1895
+ "alternate-redirect",
1896
+ "redirect-alternate",
1897
+ "redirect-intermediate"
1898
+ ]) await t.step(`${mode}: cycle`, async () => {
1899
+ esm_default.mockGlobal();
1900
+ let requests = 0;
1901
+ esm_default.get(`begin:${base}`, ({ url }) => {
1902
+ requests++;
1903
+ if (requests > 30) return Response.json({ stopped: true });
1904
+ const index = Number(new URL(url).pathname.split("/").at(-1));
1905
+ const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
1906
+ if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
1907
+ return alternate(next, mode === "html");
1908
+ });
1909
+ try {
1910
+ await rejects(loader(`${base}0`), (error) => {
1911
+ ok(error instanceof FetchError);
1912
+ ok(error.message.includes("Redirect loop detected:"));
1913
+ return true;
1914
+ });
1915
+ deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
1916
+ } finally {
1917
+ esm_default.hardReset();
1918
+ }
1919
+ });
1920
+ await t.step("alternate followed by 20 redirects shares the limit", async () => {
1921
+ esm_default.mockGlobal();
1922
+ let requests = 0;
1923
+ esm_default.get(`begin:${base}`, ({ url }) => {
1924
+ requests++;
1925
+ const index = Number(new URL(url).pathname.split("/").at(-1));
1926
+ if (index === 21) return Response.json({ done: true });
1927
+ return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
1928
+ });
1929
+ try {
1930
+ await rejects(loader(`${base}0`), (error) => {
1931
+ ok(error instanceof FetchError);
1932
+ ok(error.message.includes("Too many redirections (21)"));
1933
+ return true;
1934
+ });
1935
+ deepStrictEqual(requests, 21);
1936
+ } finally {
1937
+ esm_default.hardReset();
1938
+ }
1939
+ });
1940
+ await t.step("relative alternate after redirect and isolated calls", async () => {
1941
+ esm_default.mockGlobal();
1942
+ esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
1943
+ esm_default.get(`${base}html`, alternate("./document", true));
1944
+ esm_default.get(`${base}document`, Response.json({ done: true }));
1945
+ try {
1946
+ for (let i = 0; i < 2; i++) {
1947
+ const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
1948
+ for (const result of results) {
1949
+ deepStrictEqual(result.document, { done: true });
1950
+ deepStrictEqual(result.documentUrl, `${base}document`);
1951
+ }
1952
+ }
1953
+ } finally {
1954
+ esm_default.hardReset();
1955
+ }
1956
+ });
1957
+ function alternate(next, html) {
1958
+ return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
1959
+ "Content-Type": "text/plain",
1960
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
1961
+ } });
1962
+ }
1963
+ });
1964
+ test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
1965
+ const base = "https://example.com/alternate-abort/";
1966
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
1967
+ for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
1968
+ esm_default.mockGlobal();
1969
+ const controller = new AbortController();
1970
+ let requests = 0;
1971
+ esm_default.get(`begin:${base}`, ({ url }) => {
1972
+ requests++;
1973
+ const index = Number(new URL(url).pathname.split("/").at(-1));
1974
+ if (index === 2) return Response.json({ done: true });
1975
+ if (index === 1) controller.abort();
1976
+ const next = `${base}${index + 1}`;
1977
+ return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
1978
+ "Content-Type": "text/plain",
1979
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
1980
+ } });
1981
+ });
1982
+ try {
1983
+ await rejects(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
1984
+ deepStrictEqual(requests, 2);
1985
+ } finally {
1986
+ esm_default.hardReset();
1987
+ }
1988
+ });
1989
+ });
1990
+ test("getDocumentLoader() rejects cancellation before fetching", async () => {
1991
+ esm_default.mockGlobal();
1992
+ let requests = 0;
1993
+ const url = "https://example.com/pre-aborted-alternate";
1994
+ esm_default.get(url, () => {
1995
+ requests++;
1996
+ return Response.json({ done: true });
1997
+ });
1998
+ try {
1999
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
2000
+ const controller = new AbortController();
2001
+ controller.abort();
2002
+ await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
2003
+ deepStrictEqual(requests, 0);
2004
+ } finally {
2005
+ esm_default.hardReset();
2006
+ }
2007
+ });
2008
+ async function withServer(handler, callback) {
2009
+ const server = createServer(handler);
2010
+ await new Promise((resolve, reject) => {
2011
+ server.once("error", reject);
2012
+ server.listen(0, "127.0.0.1", resolve);
2013
+ });
2014
+ try {
2015
+ const address = server.address();
2016
+ ok(address != null && typeof address !== "string");
2017
+ return await callback(`http://127.0.0.1:${address.port}`);
2018
+ } finally {
2019
+ await new Promise((resolve, reject) => {
2020
+ server.close((error) => error == null ? resolve() : reject(error));
2021
+ server.closeAllConnections();
2022
+ });
2023
+ }
2024
+ }
2025
+ function isTimeoutFetchError(error, url) {
2026
+ ok(error instanceof FetchError, String(error));
2027
+ deepStrictEqual(error.url.href, url);
2028
+ deepStrictEqual(error.response, void 0);
2029
+ ok(error.cause instanceof DOMException);
2030
+ deepStrictEqual(error.cause.name, "TimeoutError");
2031
+ return true;
2032
+ }
2033
+ test("resolveDocumentLoaderTimeout() validates timeouts", () => {
2034
+ deepStrictEqual(resolveDocumentLoaderTimeout(void 0), 1e4);
2035
+ deepStrictEqual(resolveDocumentLoaderTimeout(null), null);
2036
+ deepStrictEqual(resolveDocumentLoaderTimeout(1500), 1500);
2037
+ deepStrictEqual(resolveDocumentLoaderTimeout(.5), 1);
2038
+ deepStrictEqual(resolveDocumentLoaderTimeout(2147483647), 2147483647);
2039
+ for (const invalid of [
2040
+ 0,
2041
+ -1,
2042
+ NaN,
2043
+ Infinity,
2044
+ 2147483648
2045
+ ]) throws(() => resolveDocumentLoaderTimeout(invalid), RangeError);
2046
+ throws(() => getDocumentLoader({ timeout: 0 }), RangeError);
2047
+ });
2048
+ test("getDocumentLoader() times out a request without a response", async () => {
2049
+ await withServer(() => {}, async (baseUrl) => {
2050
+ const url = `${baseUrl}/hang`;
2051
+ await rejects(getDocumentLoader({
2052
+ allowPrivateAddress: true,
2053
+ timeout: 100
2054
+ })(url), (e) => isTimeoutFetchError(e, url));
2055
+ });
2056
+ });
2057
+ test("getDocumentLoader() times out a stalled response body", async () => {
2058
+ for (const status of [200, 404]) await withServer((_, response) => {
2059
+ response.writeHead(status, { "Content-Type": "application/activity+json" });
2060
+ response.write("{\"id\":");
2061
+ }, async (baseUrl) => {
2062
+ const url = `${baseUrl}/stalled`;
2063
+ await rejects(getDocumentLoader({
2064
+ allowPrivateAddress: true,
2065
+ timeout: 200
2066
+ })(url), (e) => isTimeoutFetchError(e, url));
2067
+ });
2068
+ });
2069
+ test("getDocumentLoader() shares the timeout across redirects", async () => {
2070
+ let requests = 0;
2071
+ await withServer((request, response) => {
2072
+ requests++;
2073
+ const index = Number(request.url?.split("/").at(-1));
2074
+ setTimeout(() => {
2075
+ if (index >= 15) {
2076
+ response.writeHead(200, { "Content-Type": "application/activity+json" });
2077
+ response.end("{\"done\":true}");
2078
+ return;
2079
+ }
2080
+ response.writeHead(302, { Location: `/redirect/${index + 1}` });
2081
+ response.end();
2082
+ }, 50);
2083
+ }, async (baseUrl) => {
2084
+ const url = `${baseUrl}/redirect/0`;
2085
+ await rejects(getDocumentLoader({
2086
+ allowPrivateAddress: true,
2087
+ timeout: 300
2088
+ })(url), (e) => isTimeoutFetchError(e, url));
2089
+ ok(requests < 16, `requests: ${requests}`);
2090
+ deepStrictEqual((await getDocumentLoader({
2091
+ allowPrivateAddress: true,
2092
+ timeout: null
2093
+ })(url)).document, { done: true });
2094
+ });
2095
+ });
2096
+ test("getDocumentLoader() shares the timeout across alternates", async (t) => {
2097
+ for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
2098
+ await withServer((request, response) => {
2099
+ const index = Number(request.url?.split("/").at(-1));
2100
+ setTimeout(() => {
2101
+ if (index >= 15) {
2102
+ response.writeHead(200, { "Content-Type": "application/activity+json" });
2103
+ response.end("{\"done\":true}");
2104
+ return;
2105
+ }
2106
+ const next = `/alternate/${index + 1}`;
2107
+ response.writeHead(200, html ? { "Content-Type": "text/html" } : {
2108
+ "Content-Type": "text/plain",
2109
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
2110
+ });
2111
+ response.end(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON");
2112
+ }, 50);
2113
+ }, async (baseUrl) => {
2114
+ const url = `${baseUrl}/alternate/0`;
2115
+ await rejects(getDocumentLoader({
2116
+ allowPrivateAddress: true,
2117
+ timeout: 300
2118
+ })(url), (e) => isTimeoutFetchError(e, url));
2119
+ });
2120
+ });
2121
+ });
2122
+ test("getDocumentLoader() lets the caller's signal abort a request", async () => {
2123
+ await withServer(() => {}, async (baseUrl) => {
2124
+ const url = `${baseUrl}/hang`;
2125
+ for (const timeout of [
2126
+ void 0,
2127
+ 6e4,
2128
+ null
2129
+ ]) {
2130
+ const loader = getDocumentLoader({
2131
+ allowPrivateAddress: true,
2132
+ timeout
2133
+ });
2134
+ const controller = new AbortController();
2135
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
2136
+ setTimeout(() => controller.abort(reason), 50);
2137
+ await rejects(loader(url, { signal: controller.signal }), (e) => {
2138
+ deepStrictEqual(e, reason);
2139
+ return true;
2140
+ });
2141
+ }
2142
+ const loader = getDocumentLoader({
2143
+ allowPrivateAddress: true,
2144
+ timeout: 50
2145
+ });
2146
+ const controller = new AbortController();
2147
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
2148
+ controller.abort(reason);
2149
+ await rejects(loader(url, { signal: controller.signal }), (e) => {
2150
+ deepStrictEqual(e, reason);
2151
+ return true;
2152
+ });
2153
+ });
2154
+ });
2155
+ test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async () => {
2156
+ if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
2157
+ esm_default.mockGlobal();
2158
+ let requests = 0;
2159
+ const url = "https://slow-dns.example/object";
2160
+ esm_default.get(url, () => {
2161
+ requests++;
2162
+ return Response.json({ id: url });
2163
+ });
2164
+ const originalLookup = dns.lookup;
2165
+ let resolveLookup = () => {};
2166
+ const lookedUp = new Promise((resolve) => resolveLookup = resolve);
2167
+ dns.lookup = (() => new Promise((resolve) => setTimeout(() => {
2168
+ resolve([{
2169
+ address: "93.184.215.14",
2170
+ family: 4
2171
+ }]);
2172
+ resolveLookup();
2173
+ }, 200)));
2174
+ try {
2175
+ await rejects(getDocumentLoader({ timeout: 50 })(url), (e) => isTimeoutFetchError(e, url));
2176
+ await lookedUp;
2177
+ await new Promise((resolve) => setTimeout(resolve, 10));
2178
+ deepStrictEqual(requests, 0);
2179
+ } finally {
2180
+ dns.lookup = originalLookup;
2181
+ esm_default.hardReset();
2182
+ }
2183
+ });
2184
+ test("withDocumentLoaderTimeout() cleans up after each call", async () => {
2185
+ const signals = [];
2186
+ const loader = withDocumentLoaderTimeout((url, options) => {
2187
+ signals.push(options.signal);
2188
+ return Promise.resolve({
2189
+ contextUrl: null,
2190
+ document: {},
2191
+ documentUrl: url
2192
+ });
2193
+ }, 50);
2194
+ deepStrictEqual((await Promise.all([loader("https://example.com/a"), loader("https://example.com/b")])).map((r) => r.documentUrl), ["https://example.com/a", "https://example.com/b"]);
2195
+ ok(signals[0] !== signals[1]);
2196
+ await new Promise((resolve) => setTimeout(resolve, 100));
2197
+ ok(signals.every((signal) => !signal.aborted));
2198
+ const inner = () => Promise.reject(/* @__PURE__ */ new Error("unused"));
2199
+ ok(withDocumentLoaderTimeout(inner, null) === inner);
2200
+ });
2201
+ test("getRemoteDocument() keeps error bodies byte for byte", async () => {
2202
+ const bytes = new Uint8Array([
2203
+ 239,
2204
+ 187,
2205
+ 191,
2206
+ 78,
2207
+ 111,
2208
+ 255,
2209
+ 254
2210
+ ]);
2211
+ const url = "https://example.com/error";
2212
+ let error;
2213
+ await rejects(getRemoteDocument(url, new Response(bytes, { status: 404 }), () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
2214
+ error = e;
2215
+ return true;
2216
+ });
2217
+ ok(error instanceof FetchError);
2218
+ ok(error.response != null);
2219
+ deepStrictEqual(error.response.status, 404);
2220
+ deepStrictEqual(new Uint8Array(await error.response.arrayBuffer()), bytes);
2221
+ });
2222
+ test("getRemoteDocument() keeps metadata of bodiless or large errors", async () => {
2223
+ const url = "https://example.com/error";
2224
+ for (const response of [new Response(null, {
2225
+ status: 304,
2226
+ headers: { ETag: "\"a\"" }
2227
+ }), new Response(/* @__PURE__ */ new Uint8Array(1048577), {
2228
+ status: 500,
2229
+ headers: { ETag: "\"a\"" }
2230
+ })]) await rejects(getRemoteDocument(url, response, () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
2231
+ ok(e instanceof FetchError);
2232
+ ok(e.response != null);
2233
+ deepStrictEqual(e.response.status, response.status);
2234
+ deepStrictEqual(e.response.headers.get("ETag"), "\"a\"");
2235
+ deepStrictEqual(e.response.body, null);
2236
+ return true;
2237
+ });
2238
+ });
2239
+ test("getDocumentLoader() reports statuses that Response cannot hold", async () => {
2240
+ await withServer((_, response) => {
2241
+ response.writeHead(999);
2242
+ response.end("Request denied");
2243
+ }, async (baseUrl) => {
2244
+ const url = `${baseUrl}/denied`;
2245
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
2246
+ let error;
2247
+ await rejects(loader(url), (e) => {
2248
+ error = e;
2249
+ return true;
2250
+ });
2251
+ ok(error instanceof FetchError, String(error));
2252
+ deepStrictEqual(error.response?.status, 999);
2253
+ deepStrictEqual(await error.response.text(), "Request denied");
2254
+ });
2255
+ });
2256
+ test("getDocumentLoader() times out a stalled body of such a status", async () => {
2257
+ await withServer((_, response) => {
2258
+ response.writeHead(999);
2259
+ response.write("Request");
2260
+ }, async (baseUrl) => {
2261
+ const url = `${baseUrl}/stalled-denied`;
2262
+ await rejects(getDocumentLoader({
2263
+ allowPrivateAddress: true,
2264
+ timeout: 200
2265
+ })(url), (e) => isTimeoutFetchError(e, url));
2266
+ });
2267
+ });
1616
2268
  //#endregion
1617
2269
  export {};