@fedify/vocab-runtime 2.4.0-pr.936.41 → 2.4.1

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 (152) hide show
  1. package/deno.json +4 -2
  2. package/dist/{tests/docloader-Ck8bcBir.mjs → contexts-B09jb9Pi.cjs} +173 -274
  3. package/dist/{tests/docloader-S_gQZ2tt.cjs → contexts-DWrj4AGR.js} +169 -293
  4. package/dist/{docloader-C_dir7Xb.d.ts → docloader-BMHljyYb.d.cts} +68 -2
  5. package/dist/{docloader-D2DTRiyA.d.cts → docloader-BMHljyYb.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 +762 -4529
  20. package/dist/mod.d.cts +427 -9
  21. package/dist/mod.d.ts +427 -10
  22. package/dist/mod.js +742 -4524
  23. package/dist/portable-6Aj-7dZJ.d.cts +160 -0
  24. package/dist/portable-nkTiqH0w.d.ts +160 -0
  25. package/dist/tests/body-DpC9p-iG.mjs +130 -0
  26. package/dist/tests/body-cKLBprYm.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-C9Lj11SC.cjs +4533 -0
  32. package/dist/tests/contexts-duRA_dDC.mjs +4522 -0
  33. package/dist/tests/decimal.test.cjs +9 -7
  34. package/dist/tests/decimal.test.mjs +9 -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-DyZ1JGIl.cjs +398 -0
  42. package/dist/tests/docloader-tUmMeXjU.mjs +375 -0
  43. package/dist/tests/docloader.test.cjs +816 -39
  44. package/dist/tests/docloader.test.mjs +810 -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-CmDhHNTx.cjs +12 -0
  52. package/dist/tests/jsonld-DweIJ07b.mjs +5 -0
  53. package/dist/tests/jsonld-cache-BEXiiM-V.cjs +386 -0
  54. package/dist/tests/jsonld-cache-DFnWsLuZ.mjs +339 -0
  55. package/dist/tests/jsonld-cache.test.cjs +102 -298
  56. package/dist/tests/jsonld-cache.test.mjs +95 -289
  57. package/dist/tests/{key-_wXwomh_.cjs → key-C-AYkdJJ.cjs} +11 -4
  58. package/dist/tests/{key-CDGDH_vC.mjs → key-C2Db_TAJ.mjs} +11 -3
  59. package/dist/tests/key.test.cjs +6 -5
  60. package/dist/tests/key.test.mjs +6 -4
  61. package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DYRmJqFn.mjs} +5 -2
  62. package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-_PqsL1ae.cjs} +5 -2
  63. package/dist/tests/langstr.test.cjs +11 -4
  64. package/dist/tests/langstr.test.mjs +9 -2
  65. package/dist/tests/langtag-CUppHs_s.mjs +45 -0
  66. package/dist/tests/langtag-Czjvxfzx.cjs +50 -0
  67. package/dist/tests/langtag.test.cjs +45 -0
  68. package/dist/tests/langtag.test.d.cts +1 -0
  69. package/dist/tests/langtag.test.d.mts +1 -0
  70. package/dist/tests/langtag.test.mjs +46 -0
  71. package/dist/tests/link.test.cjs +2 -3
  72. package/dist/tests/link.test.mjs +2 -2
  73. package/dist/tests/multibase/multibase.test.cjs +8 -9
  74. package/dist/tests/multibase/multibase.test.mjs +6 -6
  75. package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-B5Mea7Ip.cjs} +19 -2
  76. package/dist/tests/{multibase-B4bvakyA.mjs → multibase-BPnF_L4e.mjs} +12 -1
  77. package/dist/tests/portable-dereference-DmTYDy2p.mjs +250 -0
  78. package/dist/tests/portable-dereference-qTLMqe2_.cjs +279 -0
  79. package/dist/tests/portable-media-5p8azal0.cjs +153 -0
  80. package/dist/tests/portable-media-B_lt9MMA.mjs +148 -0
  81. package/dist/tests/portable-media.test.cjs +222 -0
  82. package/dist/tests/portable-media.test.d.cts +1 -0
  83. package/dist/tests/portable-media.test.d.mts +1 -0
  84. package/dist/tests/portable-media.test.mjs +223 -0
  85. package/dist/tests/portable-workers.test.cjs +36 -0
  86. package/dist/tests/portable-workers.test.d.cts +2 -0
  87. package/dist/tests/portable-workers.test.d.mts +2 -0
  88. package/dist/tests/portable-workers.test.mjs +35 -0
  89. package/dist/tests/{request-uk51rkhO.cjs → request-BgjA9wyl.cjs} +10 -4
  90. package/dist/tests/{request-C8CaGwtt.mjs → request-DodZNukP.mjs} +8 -2
  91. package/dist/tests/request.test.cjs +7 -4
  92. package/dist/tests/request.test.mjs +5 -2
  93. package/dist/tests/signed-representation.test.cjs +588 -0
  94. package/dist/tests/signed-representation.test.d.cts +1 -0
  95. package/dist/tests/signed-representation.test.d.mts +1 -0
  96. package/dist/tests/signed-representation.test.mjs +589 -0
  97. package/dist/tests/temporal.test.cjs +1 -2
  98. package/dist/tests/temporal.test.mjs +1 -1
  99. package/dist/tests/url-BNakuZ8k.cjs +998 -0
  100. package/dist/tests/url-DMxmp7ZG.mjs +859 -0
  101. package/dist/tests/url.test.cjs +489 -6
  102. package/dist/tests/url.test.mjs +489 -5
  103. package/dist/url-DrGTR8yv.cjs +993 -0
  104. package/dist/url-Dzyp-NsC.js +860 -0
  105. package/package.json +27 -4
  106. package/scripts/test-bun.mjs +17 -0
  107. package/src/body.test.ts +125 -0
  108. package/src/body.ts +152 -0
  109. package/src/contexts/cid-v1.json +114 -0
  110. package/src/contexts/fep-22cd.json +21 -0
  111. package/src/contexts/fep-6757.json +12 -0
  112. package/src/contexts/joinmastodon.json +1 -1
  113. package/src/contexts/miscellany.json +17 -0
  114. package/src/contexts.ts +47 -1
  115. package/src/digest.test.ts +220 -0
  116. package/src/digest.ts +229 -0
  117. package/src/docloader.test.ts +1066 -27
  118. package/src/docloader.ts +322 -105
  119. package/src/internal/jsonld-cache.ts +97 -1
  120. package/src/internal/portable-dereference.test.ts +254 -0
  121. package/src/internal/portable-dereference.ts +1000 -0
  122. package/src/internal/signed-representation.ts +565 -0
  123. package/src/jsonld-cache.test.ts +89 -0
  124. package/src/key.test.ts +9 -0
  125. package/src/key.ts +11 -1
  126. package/src/langstr.test.ts +18 -1
  127. package/src/langstr.ts +6 -2
  128. package/src/langtag.test.ts +53 -0
  129. package/src/langtag.ts +76 -0
  130. package/src/mod.ts +29 -0
  131. package/src/multibase/multibase.test.ts +5 -5
  132. package/src/portable-media.test.ts +293 -0
  133. package/src/portable-media.ts +264 -0
  134. package/src/portable-workers.test.ts +81 -0
  135. package/src/portable.ts +180 -0
  136. package/src/preprocessor.ts +7 -0
  137. package/src/request.test.ts +10 -1
  138. package/src/request.ts +9 -1
  139. package/src/signed-representation.test.ts +338 -0
  140. package/src/url.test.ts +823 -3
  141. package/src/url.ts +700 -23
  142. package/tsdown.config.ts +2 -0
  143. package/dist/tests/url-CsOV_B_P.cjs +0 -482
  144. package/dist/tests/url-Du7RQQgP.mjs +0 -392
  145. package/dist/url-DD4F0ULf.cjs +0 -483
  146. package/dist/url-DGVbSVVi.js +0 -393
  147. /package/dist/{chunk-M78iaK0I.cjs → rolldown-runtime-B7lfambq.cjs} +0 -0
  148. /package/dist/tests/{link-NUUWCdnK.mjs → link-Cevmc87v.mjs} +0 -0
  149. /package/dist/tests/{link-FguCydMA.cjs → link-DlKm8bEr.cjs} +0 -0
  150. /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-CLRPeW4N.cjs} +0 -0
  151. /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-CRIj_05H.mjs} +0 -0
  152. /package/dist/tests/{chunk-C2EiDwsr.cjs → rolldown-runtime-emK7D4bc.cjs} +0 -0
@@ -1,8 +1,14 @@
1
- import { n as getRemoteDocument, r as preloadedContexts, t as getDocumentLoader } from "./docloader-Ck8bcBir.mjs";
2
- import { t as FetchError } from "./request-C8CaGwtt.mjs";
3
- import { t as UrlError } from "./url-Du7RQQgP.mjs";
4
- import { deepStrictEqual, ok, rejects } from "node:assert";
5
- import { test } from "node:test";
1
+ import { t as FetchError } from "./request-DodZNukP.mjs";
2
+ import { n as cid_v1_default, t as preloadedContexts } from "./contexts-duRA_dDC.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-tUmMeXjU.mjs";
5
+ import { t as jsonld_default } from "./jsonld-DweIJ07b.mjs";
6
+ import { gzipSync } from "node:zlib";
7
+ import { configure, reset } from "@logtape/logtape";
8
+ import { deepStrictEqual, ok, rejects, throws } from "node:assert";
9
+ import dns from "node:dns/promises";
10
+ import { test } from "@fedify/fixture";
11
+ import { createServer } from "node:http";
6
12
  //#region \0rolldown/runtime.js
7
13
  var __create = Object.create;
8
14
  var __defProp = Object.defineProperty;
@@ -130,27 +136,27 @@ function parse(input, loose) {
130
136
  }
131
137
  //#endregion
132
138
  //#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/TypeDescriptor.js
133
- const valueTypes = new Set([
139
+ const valueTypes = /* @__PURE__ */ new Set([
134
140
  "boolean",
135
141
  "number",
136
142
  "null",
137
143
  "string",
138
144
  "undefined"
139
145
  ]);
140
- const referenceTypes = new Set([
146
+ const referenceTypes = /* @__PURE__ */ new Set([
141
147
  "array",
142
148
  "function",
143
149
  "object",
144
150
  "symbol"
145
151
  ]);
146
- const detectableTypes = new Set([
152
+ const detectableTypes = /* @__PURE__ */ new Set([
147
153
  "boolean",
148
154
  "function",
149
155
  "number",
150
156
  "string",
151
157
  "symbol"
152
158
  ]);
153
- const typeConstructors = new Set([
159
+ const typeConstructors = /* @__PURE__ */ new Set([
154
160
  Boolean,
155
161
  Number,
156
162
  String
@@ -221,7 +227,7 @@ var TypeDescriptor = class TypeDescriptor {
221
227
  };
222
228
  //#endregion
223
229
  //#region ../../node_modules/.pnpm/fetch-mock@12.6.0/node_modules/fetch-mock/dist/esm/IsSubsetOf.js
224
- const allowedTypes = new Set([
230
+ const allowedTypes = /* @__PURE__ */ new Set([
225
231
  "array",
226
232
  "object",
227
233
  "function",
@@ -713,7 +719,14 @@ var __classPrivateFieldGet = function(receiver, state, kind, f) {
713
719
  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
720
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
715
721
  };
716
- var _Route_instances, _a, _Route_responseSubscriptions, _Route_validate, _Route_sanitize, _Route_generateMatcher, _Route_limit, _Route_delayResponse;
722
+ var _Route_instances;
723
+ var _a;
724
+ var _Route_responseSubscriptions;
725
+ var _Route_validate;
726
+ var _Route_sanitize;
727
+ var _Route_generateMatcher;
728
+ var _Route_limit;
729
+ var _Route_delayResponse;
717
730
  var RouteConfigWrapper = class {
718
731
  constructor(config) {
719
732
  Object.assign(this, config);
@@ -1225,6 +1238,52 @@ var esm_default = new class FetchMock {
1225
1238
  }({ ...defaultFetchMockConfig });
1226
1239
  //#endregion
1227
1240
  //#region src/docloader.test.ts
1241
+ test("getRemoteDocument() forwards options to alternate documents", async () => {
1242
+ const records = [];
1243
+ await reset();
1244
+ await configure({
1245
+ sinks: { capture: (record) => records.push(record) },
1246
+ loggers: [{
1247
+ category: ["fedify"],
1248
+ lowestLevel: "debug",
1249
+ sinks: ["capture"]
1250
+ }]
1251
+ });
1252
+ try {
1253
+ for (const linkHeader of [true, false]) for (const suppressError of [
1254
+ true,
1255
+ false,
1256
+ void 0
1257
+ ]) {
1258
+ records.length = 0;
1259
+ const url = "https://example.com/alternate-source";
1260
+ const alternateUrl = "https://example.com/missing-alternate";
1261
+ const options = {
1262
+ suppressError,
1263
+ signal: new AbortController().signal
1264
+ };
1265
+ const response = new Response(`<link rel="alternate" type="application/activity+json" href="${alternateUrl}">`, { headers: {
1266
+ "Content-Type": "text/html",
1267
+ ...linkHeader ? { Link: `<${alternateUrl}>; rel="alternate"; type="application/activity+json"` } : {}
1268
+ } });
1269
+ let alternateRequests = 0;
1270
+ await rejects(() => getRemoteDocument(url, response, async (requestedUrl, forwarded) => {
1271
+ alternateRequests++;
1272
+ deepStrictEqual(requestedUrl, alternateUrl);
1273
+ deepStrictEqual(forwarded, options);
1274
+ return await getRemoteDocument(requestedUrl, new Response(null, { status: 404 }), () => {
1275
+ throw new Error("Unexpected additional alternate");
1276
+ }, forwarded);
1277
+ }, options), FetchError);
1278
+ deepStrictEqual(alternateRequests, 1);
1279
+ const failures = records.filter((record) => record.rawMessage === "Failed to fetch document: {status} {url} {headers}");
1280
+ deepStrictEqual(failures.length, 1);
1281
+ deepStrictEqual(failures[0].level, suppressError ? "warning" : "error");
1282
+ }
1283
+ } finally {
1284
+ await reset();
1285
+ }
1286
+ });
1228
1287
  test("new FetchError()", () => {
1229
1288
  const e = new FetchError("https://example.com/", "An error message.");
1230
1289
  deepStrictEqual(e.name, "FetchError");
@@ -1247,7 +1306,7 @@ test("getDocumentLoader()", async (t) => {
1247
1306
  name: "Fetched object",
1248
1307
  type: "Object"
1249
1308
  } });
1250
- await t.test("ok", async () => {
1309
+ await t.step("ok", async () => {
1251
1310
  deepStrictEqual(await fetchDocumentLoader("https://example.com/object"), {
1252
1311
  contextUrl: null,
1253
1312
  documentUrl: "https://example.com/object",
@@ -1290,7 +1349,7 @@ test("getDocumentLoader()", async (t) => {
1290
1349
  Link: "<https://example.com/object>; rel=\"alternate\"; type=\"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"\""
1291
1350
  }
1292
1351
  });
1293
- await t.test("Link header", async () => {
1352
+ await t.step("Link header", async () => {
1294
1353
  deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
1295
1354
  contextUrl: "https://www.w3.org/ns/activitystreams",
1296
1355
  documentUrl: "https://example.com/link-ctx",
@@ -1311,7 +1370,7 @@ test("getDocumentLoader()", async (t) => {
1311
1370
  }
1312
1371
  });
1313
1372
  });
1314
- await t.test("Link header relative url", async () => {
1373
+ await t.step("Link header relative url", async () => {
1315
1374
  deepStrictEqual(await fetchDocumentLoader("https://example.com/link-ctx"), {
1316
1375
  contextUrl: "https://www.w3.org/ns/activitystreams",
1317
1376
  documentUrl: "https://example.com/link-ctx",
@@ -1332,7 +1391,7 @@ test("getDocumentLoader()", async (t) => {
1332
1391
  }
1333
1392
  });
1334
1393
  });
1335
- await t.test("wrong Link header syntax", async () => {
1394
+ await t.step("wrong Link header syntax", async () => {
1336
1395
  deepStrictEqual(await fetchDocumentLoader("https://example.com/obj-w-wrong-link"), {
1337
1396
  contextUrl: null,
1338
1397
  documentUrl: "https://example.com/obj-w-wrong-link",
@@ -1356,7 +1415,7 @@ test("getDocumentLoader()", async (t) => {
1356
1415
  </html>`,
1357
1416
  headers: { "Content-Type": "text/html; charset=utf-8" }
1358
1417
  });
1359
- await t.test("HTML <link>", async () => {
1418
+ await t.step("HTML <link>", async () => {
1360
1419
  deepStrictEqual(await fetchDocumentLoader("https://example.com/html-link"), {
1361
1420
  contextUrl: null,
1362
1421
  documentUrl: "https://example.com/object",
@@ -1380,7 +1439,7 @@ test("getDocumentLoader()", async (t) => {
1380
1439
  </html>`,
1381
1440
  headers: { "Content-Type": "application/xhtml+xml; charset=utf-8" }
1382
1441
  });
1383
- await t.test("XHTML <link>", async () => {
1442
+ await t.step("XHTML <link>", async () => {
1384
1443
  deepStrictEqual(await fetchDocumentLoader("https://example.com/xhtml-link"), {
1385
1444
  contextUrl: null,
1386
1445
  documentUrl: "https://example.com/object",
@@ -1406,7 +1465,7 @@ test("getDocumentLoader()", async (t) => {
1406
1465
  </html>`,
1407
1466
  headers: { "Content-Type": "text/html; charset=utf-8" }
1408
1467
  });
1409
- await t.test("HTML <a>", async () => {
1468
+ await t.step("HTML <a>", async () => {
1410
1469
  deepStrictEqual(await fetchDocumentLoader("https://example.com/html-a"), {
1411
1470
  contextUrl: null,
1412
1471
  documentUrl: "https://example.com/object",
@@ -1428,7 +1487,7 @@ test("getDocumentLoader()", async (t) => {
1428
1487
  </html>`,
1429
1488
  headers: { "Content-Type": "text/html; charset=utf-8" }
1430
1489
  });
1431
- await t.test("HTML without ActivityPub alternate link", async () => {
1490
+ await t.step("HTML without ActivityPub alternate link", async () => {
1432
1491
  await rejects(() => fetchDocumentLoader("https://example.com/html-no-alternate"), (error) => {
1433
1492
  ok(error instanceof FetchError);
1434
1493
  ok(error.message.includes("HTML document has no ActivityPub alternate link"));
@@ -1448,7 +1507,7 @@ test("getDocumentLoader()", async (t) => {
1448
1507
  },
1449
1508
  headers: { "Content-Type": "text/html; charset=utf-8" }
1450
1509
  });
1451
- await t.test("wrong Content-Type with JSON body", async () => {
1510
+ await t.step("wrong Content-Type with JSON body", async () => {
1452
1511
  deepStrictEqual(await fetchDocumentLoader("https://example.com/wrong-content-type"), {
1453
1512
  contextUrl: null,
1454
1513
  documentUrl: "https://example.com/wrong-content-type",
@@ -1467,7 +1526,7 @@ test("getDocumentLoader()", async (t) => {
1467
1526
  "Content-Type": "text/html; charset=utf-8"
1468
1527
  }
1469
1528
  });
1470
- await t.test("HTML Content-Length over limit", async () => {
1529
+ await t.step("HTML Content-Length over limit", async () => {
1471
1530
  await rejects(() => fetchDocumentLoader("https://example.com/large-html"), (error) => {
1472
1531
  ok(error instanceof FetchError);
1473
1532
  ok(error.message.includes("HTML document is too large to scan for an ActivityPub alternate link"));
@@ -1477,32 +1536,211 @@ test("getDocumentLoader()", async (t) => {
1477
1536
  return true;
1478
1537
  });
1479
1538
  });
1480
- await t.test("HTML Content-Length over limit cancels body", async () => {
1539
+ await t.step("HTML Content-Length over limit cancels body", async () => {
1481
1540
  let canceled = false;
1482
1541
  const response = new Response("<!DOCTYPE html>", { headers: {
1483
1542
  "Content-Length": String(1048577),
1484
1543
  "Content-Type": "text/html; charset=utf-8"
1485
1544
  } });
1486
- Object.defineProperty(response, "body", { value: { cancel: () => {
1545
+ Object.defineProperty(response, "body", { value: new ReadableStream({ cancel() {
1487
1546
  canceled = true;
1488
- } } });
1547
+ } }) });
1489
1548
  await rejects(() => getRemoteDocument("https://example.com/large-html-cancel", response, () => {
1490
1549
  throw new Error("unexpected alternate fetch");
1491
1550
  }), FetchError);
1492
1551
  deepStrictEqual(canceled, true);
1493
1552
  });
1494
1553
  esm_default.get("https://example.com/404", { status: 404 });
1495
- await t.test("not ok", async () => {
1554
+ await t.step("not ok", async () => {
1496
1555
  await rejects(() => fetchDocumentLoader("https://example.com/404"), FetchError, "HTTP 404: https://example.com/404");
1497
1556
  });
1498
- await t.test("preloaded contexts", async () => {
1557
+ await t.step("preloaded contexts", async () => {
1499
1558
  for (const [url, document] of Object.entries(preloadedContexts)) deepStrictEqual(await fetchDocumentLoader(url), {
1500
1559
  contextUrl: null,
1501
1560
  documentUrl: url,
1502
1561
  document
1503
1562
  });
1504
1563
  });
1505
- await t.test("deny non-HTTP/HTTPS", async () => {
1564
+ await t.step("preloaded Mastodon attribution domains", async () => {
1565
+ const context = ["https://www.w3.org/ns/activitystreams", "http://joinmastodon.org/ns"];
1566
+ const expanded = await jsonld_default.expand({
1567
+ "@context": context,
1568
+ id: "https://social.example/users/alice",
1569
+ type: "Person",
1570
+ attributionDomains: ["blog.example"]
1571
+ }, { documentLoader: fetchDocumentLoader });
1572
+ deepStrictEqual(expanded, [{
1573
+ "@id": "https://social.example/users/alice",
1574
+ "@type": ["https://www.w3.org/ns/activitystreams#Person"],
1575
+ "http://joinmastodon.org/ns#attributionDomains": [{ "@value": "blog.example" }]
1576
+ }]);
1577
+ deepStrictEqual((await jsonld_default.compact(expanded, context, { documentLoader: fetchDocumentLoader })).attributionDomains, ["blog.example"]);
1578
+ });
1579
+ esm_default.get("https://www.w3.org/ns/cid/v1", { status: 503 });
1580
+ await t.step("preloaded CID v1 context", async () => {
1581
+ const url = "https://www.w3.org/ns/cid/v1";
1582
+ deepStrictEqual(await fetchDocumentLoader(url), {
1583
+ contextUrl: null,
1584
+ documentUrl: url,
1585
+ document: cid_v1_default
1586
+ });
1587
+ });
1588
+ await t.step("preloaded FEP-ef61 context", async () => {
1589
+ const url = "https://w3id.org/fep/ef61";
1590
+ ok(url in preloadedContexts);
1591
+ deepStrictEqual(await fetchDocumentLoader(url), {
1592
+ contextUrl: null,
1593
+ documentUrl: url,
1594
+ document: { "@context": {
1595
+ gateways: {
1596
+ "@id": "https://w3id.org/fep/ef61/gateways",
1597
+ "@type": "@id",
1598
+ "@container": "@list"
1599
+ },
1600
+ digestMultibase: "https://www.w3.org/ns/credentials/v2#digestMultibase"
1601
+ } }
1602
+ });
1603
+ });
1604
+ esm_default.get("https://w3id.org/fep/7aa9", { status: 502 });
1605
+ await t.step("preloaded FEP-7aa9 context", async () => {
1606
+ const url = "https://w3id.org/fep/7aa9";
1607
+ deepStrictEqual(await fetchDocumentLoader(url), {
1608
+ contextUrl: null,
1609
+ documentUrl: url,
1610
+ document: { "@context": {
1611
+ "FeaturedCollection": "https://w3id.org/fep/7aa9#FeaturedCollection",
1612
+ "FeaturedItem": "https://w3id.org/fep/7aa9#FeaturedItem",
1613
+ "FeatureRequest": "https://w3id.org/fep/7aa9#FeatureRequest",
1614
+ "FeatureAuthorization": "https://w3id.org/fep/7aa9#FeatureAuthorization",
1615
+ "topic": {
1616
+ "@id": "https://w3id.org/fep/7aa9#topic",
1617
+ "@type": "@id"
1618
+ },
1619
+ "featuredObject": {
1620
+ "@id": "https://w3id.org/fep/7aa9#featuredObject",
1621
+ "@type": "@id"
1622
+ },
1623
+ "canFeature": {
1624
+ "@id": "https://w3id.org/fep/7aa9#canFeature",
1625
+ "@type": "@id"
1626
+ },
1627
+ "featureAuthorization": {
1628
+ "@id": "https://w3id.org/fep/7aa9#featureAuthorization",
1629
+ "@type": "@id"
1630
+ }
1631
+ } }
1632
+ });
1633
+ deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
1634
+ });
1635
+ esm_default.get("https://w3id.org/fep/6757", { status: 502 });
1636
+ await t.step("preloaded FEP-6757 context", async () => {
1637
+ const url = "https://w3id.org/fep/6757";
1638
+ deepStrictEqual(await fetchDocumentLoader(url), {
1639
+ contextUrl: null,
1640
+ documentUrl: url,
1641
+ document: { "@context": {
1642
+ license: {
1643
+ "@id": "http://purl.org/dc/terms/license",
1644
+ "@type": "@id"
1645
+ },
1646
+ preferredLicense: {
1647
+ "@id": "https://w3id.org/fep/6757#preferredLicense",
1648
+ "@type": "@id"
1649
+ }
1650
+ } }
1651
+ });
1652
+ deepStrictEqual(await jsonld_default.expand({
1653
+ "@context": ["https://www.w3.org/ns/activitystreams", url],
1654
+ type: "Person",
1655
+ license: "https://creativecommons.org/licenses/by/4.0/",
1656
+ preferredLicense: "https://creativecommons.org/licenses/by-nc/4.0/"
1657
+ }, { documentLoader: fetchDocumentLoader }), [{
1658
+ "@type": ["https://www.w3.org/ns/activitystreams#Person"],
1659
+ "http://purl.org/dc/terms/license": [{ "@id": "https://creativecommons.org/licenses/by/4.0/" }],
1660
+ "https://w3id.org/fep/6757#preferredLicense": [{ "@id": "https://creativecommons.org/licenses/by-nc/4.0/" }]
1661
+ }]);
1662
+ deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
1663
+ });
1664
+ esm_default.get("https://w3id.org/fep/22cd", { status: 502 });
1665
+ await t.step("preloaded FEP-22cd context", async () => {
1666
+ const url = "https://w3id.org/fep/22cd";
1667
+ const articleId = "https://example.com/articles/1";
1668
+ const translatorId = "https://example.com/users/alice";
1669
+ const revisionId = "https://example.com/articles/1/revisions/1";
1670
+ deepStrictEqual(await fetchDocumentLoader(url), {
1671
+ contextUrl: null,
1672
+ documentUrl: url,
1673
+ document: { "@context": {
1674
+ "fep-22cd": "https://w3id.org/fep/22cd#",
1675
+ schema: "https://schema.org/",
1676
+ xsd: "http://www.w3.org/2001/XMLSchema#",
1677
+ translations: {
1678
+ "@id": "fep-22cd:translations",
1679
+ "@container": "@set"
1680
+ },
1681
+ Translation: "fep-22cd:Translation",
1682
+ sourceUpdated: {
1683
+ "@id": "fep-22cd:sourceUpdated",
1684
+ "@type": "xsd:dateTime"
1685
+ },
1686
+ translator: {
1687
+ "@id": "schema:translator",
1688
+ "@type": "@id",
1689
+ "@container": "@set"
1690
+ },
1691
+ inLanguage: "schema:inLanguage",
1692
+ translationOfWork: {
1693
+ "@id": "schema:translationOfWork",
1694
+ "@type": "@id"
1695
+ },
1696
+ isBasedOn: {
1697
+ "@id": "schema:isBasedOn",
1698
+ "@type": "@id"
1699
+ }
1700
+ } }
1701
+ });
1702
+ deepStrictEqual(await jsonld_default.expand({
1703
+ "@context": ["https://www.w3.org/ns/activitystreams", url],
1704
+ id: articleId,
1705
+ type: "Article",
1706
+ contentMap: {
1707
+ en: "Original",
1708
+ ko: "Translation"
1709
+ },
1710
+ translations: [{
1711
+ type: "Translation",
1712
+ inLanguage: "ko",
1713
+ translator: [translatorId],
1714
+ translationOfWork: articleId,
1715
+ sourceUpdated: "2026-09-01T00:00:00Z",
1716
+ isBasedOn: revisionId
1717
+ }]
1718
+ }, { documentLoader: fetchDocumentLoader }), [{
1719
+ "@id": articleId,
1720
+ "@type": ["https://www.w3.org/ns/activitystreams#Article"],
1721
+ "https://www.w3.org/ns/activitystreams#content": [{
1722
+ "@language": "en",
1723
+ "@value": "Original"
1724
+ }, {
1725
+ "@language": "ko",
1726
+ "@value": "Translation"
1727
+ }],
1728
+ "https://w3id.org/fep/22cd#translations": [{
1729
+ "@type": ["https://w3id.org/fep/22cd#Translation"],
1730
+ "https://schema.org/inLanguage": [{ "@value": "ko" }],
1731
+ "https://schema.org/translator": [{ "@id": translatorId }],
1732
+ "https://schema.org/translationOfWork": [{ "@id": articleId }],
1733
+ "https://w3id.org/fep/22cd#sourceUpdated": [{
1734
+ "@type": "http://www.w3.org/2001/XMLSchema#dateTime",
1735
+ "@value": "2026-09-01T00:00:00Z"
1736
+ }],
1737
+ "https://schema.org/isBasedOn": [{ "@id": revisionId }]
1738
+ }]
1739
+ }]);
1740
+ deepStrictEqual(esm_default.callHistory.calls(url).length, 0);
1741
+ deepStrictEqual(esm_default.callHistory.calls("https://www.w3.org/ns/activitystreams").length, 0);
1742
+ });
1743
+ await t.step("deny non-HTTP/HTTPS", async () => {
1506
1744
  await rejects(() => fetchDocumentLoader("ftp://localhost"), UrlError);
1507
1745
  });
1508
1746
  esm_default.get("https://example.com/localhost-redirect", {
@@ -1527,13 +1765,13 @@ test("getDocumentLoader()", async (t) => {
1527
1765
  name: "Fetched object",
1528
1766
  type: "Object"
1529
1767
  } });
1530
- await t.test("allowPrivateAddress: false", async () => {
1768
+ await t.step("allowPrivateAddress: false", async () => {
1531
1769
  await rejects(() => fetchDocumentLoader("https://localhost/object"), UrlError);
1532
1770
  await rejects(() => fetchDocumentLoader("https://example.com/localhost-redirect"), UrlError);
1533
1771
  await rejects(() => fetchDocumentLoader("https://example.com/localhost-link"), UrlError);
1534
1772
  });
1535
1773
  const fetchDocumentLoader2 = getDocumentLoader({ allowPrivateAddress: true });
1536
- await t.test("allowPrivateAddress: true", async () => {
1774
+ await t.step("allowPrivateAddress: true", async () => {
1537
1775
  const expected = {
1538
1776
  contextUrl: null,
1539
1777
  documentUrl: "https://localhost/object",
@@ -1556,12 +1794,12 @@ test("getDocumentLoader()", async (t) => {
1556
1794
  headers: { Location: `https://example.com/too-many-redirects/${Number(cl.url.split("/").at(-1)) + 1}` }
1557
1795
  };
1558
1796
  });
1559
- await t.test("too many redirects", async () => {
1797
+ await t.step("too many redirects", async () => {
1560
1798
  redirectAttempts = 0;
1561
1799
  await rejects(() => fetchDocumentLoader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
1562
1800
  deepStrictEqual(redirectAttempts, 21);
1563
1801
  });
1564
- await t.test("custom max redirection", async () => {
1802
+ await t.step("custom max redirection", async () => {
1565
1803
  redirectAttempts = 0;
1566
1804
  const loader = getDocumentLoader({ maxRedirection: 1 });
1567
1805
  await rejects(() => loader("https://example.com/too-many-redirects/0"), FetchError, "Too many redirections");
@@ -1582,7 +1820,7 @@ test("getDocumentLoader()", async (t) => {
1582
1820
  headers: { Location: "https://example.com/redirect-loop-a" }
1583
1821
  };
1584
1822
  });
1585
- await t.test("redirect loop", async () => {
1823
+ await t.step("redirect loop", async () => {
1586
1824
  loopAttempts = 0;
1587
1825
  await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-a"), FetchError, "Redirect loop detected");
1588
1826
  deepStrictEqual(loopAttempts, 2);
@@ -1595,7 +1833,7 @@ test("getDocumentLoader()", async (t) => {
1595
1833
  headers: { Location: "/redirect-loop-relative" }
1596
1834
  };
1597
1835
  });
1598
- await t.test("redirect loop with relative location", async () => {
1836
+ await t.step("redirect loop with relative location", async () => {
1599
1837
  relativeLoopAttempts = 0;
1600
1838
  await rejects(() => fetchDocumentLoader("https://example.com/redirect-loop-relative"), FetchError, "Redirect loop detected");
1601
1839
  deepStrictEqual(relativeLoopAttempts, 1);
@@ -1605,7 +1843,7 @@ test("getDocumentLoader()", async (t) => {
1605
1843
  body: maliciousPayload,
1606
1844
  headers: { "Content-Type": "text/html; charset=utf-8" }
1607
1845
  });
1608
- await t.test("ReDoS resistance (CVE-2025-68475)", async () => {
1846
+ await t.step("ReDoS resistance (CVE-2025-68475)", async () => {
1609
1847
  const start = performance.now();
1610
1848
  await rejects(() => fetchDocumentLoader("https://example.com/redos"), FetchError);
1611
1849
  const elapsed = performance.now() - start;
@@ -1613,5 +1851,543 @@ test("getDocumentLoader()", async (t) => {
1613
1851
  });
1614
1852
  esm_default.hardReset();
1615
1853
  });
1854
+ test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
1855
+ const url = "https://example.com/bounded";
1856
+ esm_default.mockGlobal();
1857
+ let oversized = true;
1858
+ try {
1859
+ esm_default.get(url, () => new Response("{\"name\":\"hello\"}", { headers: {
1860
+ "Content-Type": "application/activity+json",
1861
+ ...oversized ? { "Content-Length": String(16777217) } : {}
1862
+ } }));
1863
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
1864
+ await rejects(loader(url), FetchError);
1865
+ oversized = false;
1866
+ deepStrictEqual((await loader(url)).document, { name: "hello" });
1867
+ oversized = true;
1868
+ esm_default.get(`${url}/html`, () => new Response(" ".repeat(1048577), { headers: { "Content-Type": "text/html" } }));
1869
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`), FetchError);
1870
+ esm_default.get(`${url}/alternate`, () => new Response("", { headers: {
1871
+ "Content-Type": "text/html",
1872
+ Link: `<${url}>; rel="alternate"; type="application/activity+json"`
1873
+ } }));
1874
+ await rejects(loader(`${url}/alternate`), FetchError);
1875
+ esm_default.get(`${url}/redirect`, {
1876
+ status: 302,
1877
+ headers: { Location: url }
1878
+ });
1879
+ await rejects(loader(`${url}/redirect`), FetchError);
1880
+ } finally {
1881
+ esm_default.hardReset();
1882
+ }
1883
+ });
1884
+ test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
1885
+ const document = { name: "a".repeat(16 * 1024 * 1024) };
1886
+ const compressed = gzipSync(JSON.stringify(document));
1887
+ ok(compressed.byteLength < 64 * 1024);
1888
+ const server = createServer((request, response) => {
1889
+ const body = request.url === "/small" ? gzipSync("{\"name\":\"hello\"}") : compressed;
1890
+ response.writeHead(200, {
1891
+ "Content-Type": "application/activity+json",
1892
+ "Content-Encoding": "gzip",
1893
+ "Content-Length": body.byteLength
1894
+ });
1895
+ response.end(body);
1896
+ });
1897
+ await new Promise((resolve, reject) => {
1898
+ server.once("error", reject);
1899
+ server.listen(0, "127.0.0.1", resolve);
1900
+ });
1901
+ try {
1902
+ const address = server.address();
1903
+ ok(address != null && typeof address !== "string");
1904
+ const url = `http://127.0.0.1:${address.port}/document`;
1905
+ await rejects(getDocumentLoader({ allowPrivateAddress: true })(url), FetchError);
1906
+ deepStrictEqual((await getDocumentLoader({ allowPrivateAddress: true })(new URL("/small", url).href)).document, { name: "hello" });
1907
+ } finally {
1908
+ await new Promise((resolve, reject) => {
1909
+ server.close((error) => error == null ? resolve() : reject(error));
1910
+ server.closeAllConnections();
1911
+ });
1912
+ }
1913
+ });
1914
+ test("getRemoteDocument() bounds JSON by default", async () => {
1915
+ let canceled = false;
1916
+ let pulls = 0;
1917
+ await rejects(getRemoteDocument("https://example.com/document", new Response(new ReadableStream({
1918
+ pull(controller) {
1919
+ pulls++;
1920
+ controller.enqueue(new Uint8Array(64 * 1024).fill(32));
1921
+ },
1922
+ cancel() {
1923
+ canceled = true;
1924
+ }
1925
+ }, { highWaterMark: 0 })), () => {
1926
+ throw new Error("Unexpected alternate document");
1927
+ }), FetchError);
1928
+ deepStrictEqual(canceled, true);
1929
+ deepStrictEqual(pulls, 257);
1930
+ });
1931
+ test("getDocumentLoader() logs DNS failures as such", async (t) => {
1932
+ if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
1933
+ const loader = getDocumentLoader();
1934
+ for (const result of [
1935
+ "throws",
1936
+ "empty",
1937
+ "private"
1938
+ ]) await t.step(result, async () => {
1939
+ const originalLookup = dns.lookup;
1940
+ dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
1941
+ address: "127.0.0.1",
1942
+ family: 4
1943
+ }]));
1944
+ const records = [];
1945
+ await configure({
1946
+ sinks: { buffer: (record) => records.push(record) },
1947
+ loggers: [{
1948
+ category: "fedify",
1949
+ sinks: ["buffer"],
1950
+ lowestLevel: "debug"
1951
+ }, {
1952
+ category: ["logtape", "meta"],
1953
+ sinks: []
1954
+ }],
1955
+ reset: true
1956
+ });
1957
+ try {
1958
+ const url = "https://dns-failure.invalid/object";
1959
+ let error;
1960
+ await rejects(() => loader(url), (e) => {
1961
+ error = e;
1962
+ return true;
1963
+ });
1964
+ ok(error instanceof UrlError);
1965
+ deepStrictEqual(error.reason, result === "private" ? "disallowed" : "dns");
1966
+ deepStrictEqual(records.map((r) => [
1967
+ r.level,
1968
+ r.rawMessage,
1969
+ r.properties.url
1970
+ ]), [result === "private" ? [
1971
+ "error",
1972
+ "Disallowed private URL: {url}",
1973
+ url
1974
+ ] : [
1975
+ "debug",
1976
+ "DNS lookup failed for {url}",
1977
+ url
1978
+ ]]);
1979
+ ok(records[0].properties.error === error);
1980
+ } finally {
1981
+ await reset();
1982
+ dns.lookup = originalLookup;
1983
+ }
1984
+ });
1985
+ });
1986
+ test("getDocumentLoader() bounds alternate document chains", async (t) => {
1987
+ const base = "https://example.com/alternate-chain/";
1988
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
1989
+ for (const mode of [
1990
+ "header",
1991
+ "html",
1992
+ "mixed"
1993
+ ]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
1994
+ esm_default.mockGlobal();
1995
+ let requests = 0;
1996
+ esm_default.get(`begin:${base}`, ({ url }) => {
1997
+ requests++;
1998
+ const index = Number(new URL(url).pathname.split("/").at(-1));
1999
+ if (index === hops || requests > 30) return Response.json({ done: true });
2000
+ const next = `${base}${index + 1}`;
2001
+ if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
2002
+ return alternate(next, mode === "html");
2003
+ });
2004
+ try {
2005
+ if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
2006
+ else await rejects(loader(`${base}0`), (error) => {
2007
+ ok(error instanceof FetchError);
2008
+ ok(error.message.includes("Too many redirections (21)"));
2009
+ return true;
2010
+ });
2011
+ deepStrictEqual(requests, 21);
2012
+ } finally {
2013
+ esm_default.hardReset();
2014
+ }
2015
+ });
2016
+ for (const mode of [
2017
+ "header",
2018
+ "html",
2019
+ "alternate-redirect",
2020
+ "redirect-alternate",
2021
+ "redirect-intermediate"
2022
+ ]) await t.step(`${mode}: cycle`, async () => {
2023
+ esm_default.mockGlobal();
2024
+ let requests = 0;
2025
+ esm_default.get(`begin:${base}`, ({ url }) => {
2026
+ requests++;
2027
+ if (requests > 30) return Response.json({ stopped: true });
2028
+ const index = Number(new URL(url).pathname.split("/").at(-1));
2029
+ const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
2030
+ if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
2031
+ return alternate(next, mode === "html");
2032
+ });
2033
+ try {
2034
+ await rejects(loader(`${base}0`), (error) => {
2035
+ ok(error instanceof FetchError);
2036
+ ok(error.message.includes("Redirect loop detected:"));
2037
+ return true;
2038
+ });
2039
+ deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
2040
+ } finally {
2041
+ esm_default.hardReset();
2042
+ }
2043
+ });
2044
+ await t.step("alternate followed by 20 redirects shares the limit", async () => {
2045
+ esm_default.mockGlobal();
2046
+ let requests = 0;
2047
+ esm_default.get(`begin:${base}`, ({ url }) => {
2048
+ requests++;
2049
+ const index = Number(new URL(url).pathname.split("/").at(-1));
2050
+ if (index === 21) return Response.json({ done: true });
2051
+ return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
2052
+ });
2053
+ try {
2054
+ await rejects(loader(`${base}0`), (error) => {
2055
+ ok(error instanceof FetchError);
2056
+ ok(error.message.includes("Too many redirections (21)"));
2057
+ return true;
2058
+ });
2059
+ deepStrictEqual(requests, 21);
2060
+ } finally {
2061
+ esm_default.hardReset();
2062
+ }
2063
+ });
2064
+ await t.step("relative alternate after redirect and isolated calls", async () => {
2065
+ esm_default.mockGlobal();
2066
+ esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
2067
+ esm_default.get(`${base}html`, alternate("./document", true));
2068
+ esm_default.get(`${base}document`, Response.json({ done: true }));
2069
+ try {
2070
+ for (let i = 0; i < 2; i++) {
2071
+ const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
2072
+ for (const result of results) {
2073
+ deepStrictEqual(result.document, { done: true });
2074
+ deepStrictEqual(result.documentUrl, `${base}document`);
2075
+ }
2076
+ }
2077
+ } finally {
2078
+ esm_default.hardReset();
2079
+ }
2080
+ });
2081
+ function alternate(next, html) {
2082
+ return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
2083
+ "Content-Type": "text/plain",
2084
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
2085
+ } });
2086
+ }
2087
+ });
2088
+ test("getDocumentLoader() preserves cancellation across alternates", async (t) => {
2089
+ const base = "https://example.com/alternate-abort/";
2090
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
2091
+ for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
2092
+ esm_default.mockGlobal();
2093
+ const controller = new AbortController();
2094
+ let requests = 0;
2095
+ esm_default.get(`begin:${base}`, ({ url }) => {
2096
+ requests++;
2097
+ const index = Number(new URL(url).pathname.split("/").at(-1));
2098
+ if (index === 2) return Response.json({ done: true });
2099
+ if (index === 1) controller.abort();
2100
+ const next = `${base}${index + 1}`;
2101
+ return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
2102
+ "Content-Type": "text/plain",
2103
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
2104
+ } });
2105
+ });
2106
+ try {
2107
+ await rejects(loader(`${base}0`, { signal: controller.signal }), { name: "AbortError" });
2108
+ deepStrictEqual(requests, 2);
2109
+ } finally {
2110
+ esm_default.hardReset();
2111
+ }
2112
+ });
2113
+ });
2114
+ test("getDocumentLoader() rejects cancellation before fetching", async () => {
2115
+ esm_default.mockGlobal();
2116
+ let requests = 0;
2117
+ const url = "https://example.com/pre-aborted-alternate";
2118
+ esm_default.get(url, () => {
2119
+ requests++;
2120
+ return Response.json({ done: true });
2121
+ });
2122
+ try {
2123
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
2124
+ const controller = new AbortController();
2125
+ controller.abort();
2126
+ await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
2127
+ deepStrictEqual(requests, 0);
2128
+ } finally {
2129
+ esm_default.hardReset();
2130
+ }
2131
+ });
2132
+ async function withServer(handler, callback) {
2133
+ const server = createServer(handler);
2134
+ await new Promise((resolve, reject) => {
2135
+ server.once("error", reject);
2136
+ server.listen(0, "127.0.0.1", resolve);
2137
+ });
2138
+ try {
2139
+ const address = server.address();
2140
+ ok(address != null && typeof address !== "string");
2141
+ return await callback(`http://127.0.0.1:${address.port}`);
2142
+ } finally {
2143
+ await new Promise((resolve, reject) => {
2144
+ server.close((error) => error == null ? resolve() : reject(error));
2145
+ server.closeAllConnections();
2146
+ });
2147
+ }
2148
+ }
2149
+ function isTimeoutFetchError(error, url) {
2150
+ ok(error instanceof FetchError, String(error));
2151
+ deepStrictEqual(error.url.href, url);
2152
+ deepStrictEqual(error.response, void 0);
2153
+ ok(error.cause instanceof DOMException);
2154
+ deepStrictEqual(error.cause.name, "TimeoutError");
2155
+ return true;
2156
+ }
2157
+ test("resolveDocumentLoaderTimeout() validates timeouts", () => {
2158
+ deepStrictEqual(resolveDocumentLoaderTimeout(void 0), 1e4);
2159
+ deepStrictEqual(resolveDocumentLoaderTimeout(null), null);
2160
+ deepStrictEqual(resolveDocumentLoaderTimeout(1500), 1500);
2161
+ deepStrictEqual(resolveDocumentLoaderTimeout(.5), 1);
2162
+ deepStrictEqual(resolveDocumentLoaderTimeout(2147483647), 2147483647);
2163
+ for (const invalid of [
2164
+ 0,
2165
+ -1,
2166
+ NaN,
2167
+ Infinity,
2168
+ 2147483648
2169
+ ]) throws(() => resolveDocumentLoaderTimeout(invalid), RangeError);
2170
+ throws(() => getDocumentLoader({ timeout: 0 }), RangeError);
2171
+ });
2172
+ test("getDocumentLoader() times out a request without a response", async () => {
2173
+ await withServer(() => {}, async (baseUrl) => {
2174
+ const url = `${baseUrl}/hang`;
2175
+ await rejects(getDocumentLoader({
2176
+ allowPrivateAddress: true,
2177
+ timeout: 100
2178
+ })(url), (e) => isTimeoutFetchError(e, url));
2179
+ });
2180
+ });
2181
+ test("getDocumentLoader() times out a stalled response body", async () => {
2182
+ for (const status of [200, 404]) await withServer((_, response) => {
2183
+ response.writeHead(status, { "Content-Type": "application/activity+json" });
2184
+ response.write("{\"id\":");
2185
+ }, async (baseUrl) => {
2186
+ const url = `${baseUrl}/stalled`;
2187
+ await rejects(getDocumentLoader({
2188
+ allowPrivateAddress: true,
2189
+ timeout: 200
2190
+ })(url), (e) => isTimeoutFetchError(e, url));
2191
+ });
2192
+ });
2193
+ test("getDocumentLoader() shares the timeout across redirects", async () => {
2194
+ let requests = 0;
2195
+ await withServer((request, response) => {
2196
+ requests++;
2197
+ const index = Number(request.url?.split("/").at(-1));
2198
+ setTimeout(() => {
2199
+ if (index >= 15) {
2200
+ response.writeHead(200, { "Content-Type": "application/activity+json" });
2201
+ response.end("{\"done\":true}");
2202
+ return;
2203
+ }
2204
+ response.writeHead(302, { Location: `/redirect/${index + 1}` });
2205
+ response.end();
2206
+ }, 50);
2207
+ }, async (baseUrl) => {
2208
+ const url = `${baseUrl}/redirect/0`;
2209
+ await rejects(getDocumentLoader({
2210
+ allowPrivateAddress: true,
2211
+ timeout: 300
2212
+ })(url), (e) => isTimeoutFetchError(e, url));
2213
+ ok(requests < 16, `requests: ${requests}`);
2214
+ deepStrictEqual((await getDocumentLoader({
2215
+ allowPrivateAddress: true,
2216
+ timeout: null
2217
+ })(url)).document, { done: true });
2218
+ });
2219
+ });
2220
+ test("getDocumentLoader() shares the timeout across alternates", async (t) => {
2221
+ for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
2222
+ await withServer((request, response) => {
2223
+ const index = Number(request.url?.split("/").at(-1));
2224
+ setTimeout(() => {
2225
+ if (index >= 15) {
2226
+ response.writeHead(200, { "Content-Type": "application/activity+json" });
2227
+ response.end("{\"done\":true}");
2228
+ return;
2229
+ }
2230
+ const next = `/alternate/${index + 1}`;
2231
+ response.writeHead(200, html ? { "Content-Type": "text/html" } : {
2232
+ "Content-Type": "text/plain",
2233
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
2234
+ });
2235
+ response.end(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON");
2236
+ }, 50);
2237
+ }, async (baseUrl) => {
2238
+ const url = `${baseUrl}/alternate/0`;
2239
+ await rejects(getDocumentLoader({
2240
+ allowPrivateAddress: true,
2241
+ timeout: 300
2242
+ })(url), (e) => isTimeoutFetchError(e, url));
2243
+ });
2244
+ });
2245
+ });
2246
+ test("getDocumentLoader() lets the caller's signal abort a request", async () => {
2247
+ await withServer(() => {}, async (baseUrl) => {
2248
+ const url = `${baseUrl}/hang`;
2249
+ for (const timeout of [
2250
+ void 0,
2251
+ 6e4,
2252
+ null
2253
+ ]) {
2254
+ const loader = getDocumentLoader({
2255
+ allowPrivateAddress: true,
2256
+ timeout
2257
+ });
2258
+ const controller = new AbortController();
2259
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
2260
+ setTimeout(() => controller.abort(reason), 50);
2261
+ await rejects(loader(url, { signal: controller.signal }), (e) => {
2262
+ deepStrictEqual(e, reason);
2263
+ return true;
2264
+ });
2265
+ }
2266
+ const loader = getDocumentLoader({
2267
+ allowPrivateAddress: true,
2268
+ timeout: 50
2269
+ });
2270
+ const controller = new AbortController();
2271
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
2272
+ controller.abort(reason);
2273
+ await rejects(loader(url, { signal: controller.signal }), (e) => {
2274
+ deepStrictEqual(e, reason);
2275
+ return true;
2276
+ });
2277
+ });
2278
+ });
2279
+ test("getDocumentLoader() bounds DNS lookups that ignore the timeout", async () => {
2280
+ if ("Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted") throw new Error("This DNS test requires Deno network permission (--allow-net).");
2281
+ esm_default.mockGlobal();
2282
+ let requests = 0;
2283
+ const url = "https://slow-dns.example/object";
2284
+ esm_default.get(url, () => {
2285
+ requests++;
2286
+ return Response.json({ id: url });
2287
+ });
2288
+ const originalLookup = dns.lookup;
2289
+ let resolveLookup = () => {};
2290
+ const lookedUp = new Promise((resolve) => resolveLookup = resolve);
2291
+ dns.lookup = (() => new Promise((resolve) => setTimeout(() => {
2292
+ resolve([{
2293
+ address: "93.184.215.14",
2294
+ family: 4
2295
+ }]);
2296
+ resolveLookup();
2297
+ }, 200)));
2298
+ try {
2299
+ await rejects(getDocumentLoader({ timeout: 50 })(url), (e) => isTimeoutFetchError(e, url));
2300
+ await lookedUp;
2301
+ await new Promise((resolve) => setTimeout(resolve, 10));
2302
+ deepStrictEqual(requests, 0);
2303
+ } finally {
2304
+ dns.lookup = originalLookup;
2305
+ esm_default.hardReset();
2306
+ }
2307
+ });
2308
+ test("withDocumentLoaderTimeout() cleans up after each call", async () => {
2309
+ const signals = [];
2310
+ const loader = withDocumentLoaderTimeout((url, options) => {
2311
+ signals.push(options.signal);
2312
+ return Promise.resolve({
2313
+ contextUrl: null,
2314
+ document: {},
2315
+ documentUrl: url
2316
+ });
2317
+ }, 50);
2318
+ 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"]);
2319
+ ok(signals[0] !== signals[1]);
2320
+ await new Promise((resolve) => setTimeout(resolve, 100));
2321
+ ok(signals.every((signal) => !signal.aborted));
2322
+ const inner = () => Promise.reject(/* @__PURE__ */ new Error("unused"));
2323
+ ok(withDocumentLoaderTimeout(inner, null) === inner);
2324
+ });
2325
+ test("getRemoteDocument() keeps error bodies byte for byte", async () => {
2326
+ const bytes = new Uint8Array([
2327
+ 239,
2328
+ 187,
2329
+ 191,
2330
+ 78,
2331
+ 111,
2332
+ 255,
2333
+ 254
2334
+ ]);
2335
+ const url = "https://example.com/error";
2336
+ let error;
2337
+ await rejects(getRemoteDocument(url, new Response(bytes, { status: 404 }), () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
2338
+ error = e;
2339
+ return true;
2340
+ });
2341
+ ok(error instanceof FetchError);
2342
+ ok(error.response != null);
2343
+ deepStrictEqual(error.response.status, 404);
2344
+ deepStrictEqual(new Uint8Array(await error.response.arrayBuffer()), bytes);
2345
+ });
2346
+ test("getRemoteDocument() keeps metadata of bodiless or large errors", async () => {
2347
+ const url = "https://example.com/error";
2348
+ for (const response of [new Response(null, {
2349
+ status: 304,
2350
+ headers: { ETag: "\"a\"" }
2351
+ }), new Response(/* @__PURE__ */ new Uint8Array(1048577), {
2352
+ status: 500,
2353
+ headers: { ETag: "\"a\"" }
2354
+ })]) await rejects(getRemoteDocument(url, response, () => Promise.reject(/* @__PURE__ */ new Error("unused"))), (e) => {
2355
+ ok(e instanceof FetchError);
2356
+ ok(e.response != null);
2357
+ deepStrictEqual(e.response.status, response.status);
2358
+ deepStrictEqual(e.response.headers.get("ETag"), "\"a\"");
2359
+ deepStrictEqual(e.response.body, null);
2360
+ return true;
2361
+ });
2362
+ });
2363
+ test("getDocumentLoader() reports statuses that Response cannot hold", async () => {
2364
+ await withServer((_, response) => {
2365
+ response.writeHead(999);
2366
+ response.end("Request denied");
2367
+ }, async (baseUrl) => {
2368
+ const url = `${baseUrl}/denied`;
2369
+ const loader = getDocumentLoader({ allowPrivateAddress: true });
2370
+ let error;
2371
+ await rejects(loader(url), (e) => {
2372
+ error = e;
2373
+ return true;
2374
+ });
2375
+ ok(error instanceof FetchError, String(error));
2376
+ deepStrictEqual(error.response?.status, 999);
2377
+ deepStrictEqual(await error.response.text(), "Request denied");
2378
+ });
2379
+ });
2380
+ test("getDocumentLoader() times out a stalled body of such a status", async () => {
2381
+ await withServer((_, response) => {
2382
+ response.writeHead(999);
2383
+ response.write("Request");
2384
+ }, async (baseUrl) => {
2385
+ const url = `${baseUrl}/stalled-denied`;
2386
+ await rejects(getDocumentLoader({
2387
+ allowPrivateAddress: true,
2388
+ timeout: 200
2389
+ })(url), (e) => isTimeoutFetchError(e, url));
2390
+ });
2391
+ });
1616
2392
  //#endregion
1617
2393
  export {};