@fedify/fedify 2.1.23 → 2.1.25

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 (101) hide show
  1. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
  2. package/dist/{builder-BlCsjkrX.mjs → builder-DsBAef6b.mjs} +3 -3
  3. package/dist/{client-CZWLyLmp.mjs → client-DM72NZuk.mjs} +85 -7
  4. package/dist/compat/mod.d.cts +1 -1
  5. package/dist/compat/mod.d.ts +1 -1
  6. package/dist/compat/transformers.test.mjs +3 -4
  7. package/dist/{context-_l9t359k.d.ts → context-BgU5UWTx.d.ts} +4 -3
  8. package/dist/{context-o22vjzS2.d.cts → context-D4s0Wk3N.d.cts} +4 -3
  9. package/dist/{deno-B_Gwkjh7.mjs → deno-9AaebKOA.mjs} +1 -1
  10. package/dist/docloader-DX96uPUj.mjs +78 -0
  11. package/dist/federation/builder.test.mjs +3 -3
  12. package/dist/federation/collection.test.mjs +2 -2
  13. package/dist/federation/handler.test.mjs +128 -10
  14. package/dist/federation/idempotency.test.mjs +5 -5
  15. package/dist/federation/inbox.test.mjs +1 -1
  16. package/dist/federation/keycache.test.mjs +44 -10
  17. package/dist/federation/kv.test.mjs +2 -2
  18. package/dist/federation/middleware.test.mjs +227 -12
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/mq.test.mjs +99 -44
  24. package/dist/federation/negotiation.test.mjs +3 -3
  25. package/dist/federation/retry.test.mjs +1 -1
  26. package/dist/federation/router.test.mjs +2 -2
  27. package/dist/federation/send.test.mjs +202 -7
  28. package/dist/federation/temporal.test.mjs +3 -3
  29. package/dist/federation/webfinger.test.mjs +3 -3
  30. package/dist/{http-DIh9_s_-.mjs → http-B5qAeoJd.mjs} +8 -4
  31. package/dist/{http-Yoq0YGz7.cjs → http-BJEMpnnY.cjs} +171 -7
  32. package/dist/{http-88deUx3Y.js → http-CdJ6n4rA.js} +160 -8
  33. package/dist/{http-BDZeS5om.d.ts → http-Dl00HXr4.d.ts} +1 -1
  34. package/dist/{http-C87EWkO0.d.cts → http-DqZhyEso.d.cts} +1 -1
  35. package/dist/{inbox-BxCHIWPD.mjs → inbox-BSyjaFbo.mjs} +1 -1
  36. package/dist/{key-DywPPmb0.mjs → key-x9f-H7eO.mjs} +154 -6
  37. package/dist/{keycache-CCSwkQcY.mjs → keycache-CaO7wyQA.mjs} +12 -4
  38. package/dist/{kv-cache-HA011rBG.cjs → kv-cache-BDp2c_QE.cjs} +34 -8
  39. package/dist/{kv-cache-rHQOc5rE.js → kv-cache-Dx3QwNBI.js} +35 -9
  40. package/dist/{ld-Ch6FVq7X.mjs → ld-cfUwoi0C.mjs} +3 -3
  41. package/dist/{middleware-itcr4fAv.cjs → middleware-BXJpAXhO.cjs} +1 -1
  42. package/dist/{middleware-CYsQG25-.js → middleware-CP35ElZv.js} +99 -14
  43. package/dist/{middleware-DABFg_Xy.mjs → middleware-DDT9ljTQ.mjs} +1 -1
  44. package/dist/{middleware-D2O8b03q.mjs → middleware-DHHRwSZH.mjs} +31 -20
  45. package/dist/{middleware-CUNIlYzF.cjs → middleware-XXuSt_6b.cjs} +99 -14
  46. package/dist/{mod-CjiK2zrW.d.ts → mod-BLDHhuW0.d.ts} +1 -1
  47. package/dist/{mod-0dF0OeGw.d.cts → mod-Bx80LUTA.d.cts} +1 -1
  48. package/dist/{mod-Bv1elC-b.d.ts → mod-DcAiY74j.d.ts} +1 -1
  49. package/dist/{mod-D5YlzzP3.d.cts → mod-DiuFTYUt.d.cts} +1 -1
  50. package/dist/{mod-dxOnKp2Y.d.cts → mod-DkE98l_7.d.cts} +3 -1
  51. package/dist/{mod-BTWFtZwa.d.ts → mod-DqkY4FDr.d.ts} +3 -1
  52. package/dist/mod.cjs +5 -5
  53. package/dist/mod.d.cts +6 -6
  54. package/dist/mod.d.ts +6 -6
  55. package/dist/mod.js +5 -5
  56. package/dist/nodeinfo/client.test.mjs +77 -2
  57. package/dist/nodeinfo/handler.test.mjs +3 -3
  58. package/dist/nodeinfo/mod.cjs +1 -1
  59. package/dist/nodeinfo/mod.js +1 -1
  60. package/dist/nodeinfo/types.test.mjs +2 -2
  61. package/dist/otel/exporter.test.mjs +2 -2
  62. package/dist/{owner-DEvZuyOE.d.cts → owner-B3hNQCG8.d.cts} +11 -0
  63. package/dist/{owner-CnngXDNJ.d.ts → owner-CiBPcC6F.d.ts} +11 -0
  64. package/dist/owner-DklgvZXn.mjs +175 -0
  65. package/dist/{proof-Dt5GV-xi.mjs → proof-CI4rsVLu.mjs} +2 -2
  66. package/dist/{proof-K3inAqnp.js → proof-Dsa_ZC-M.js} +113 -59
  67. package/dist/{proof-CEn2R6W9.cjs → proof-DzFzyfIc.cjs} +113 -59
  68. package/dist/{send-CqaJDT2z.mjs → send-DJmW6Z-I.mjs} +73 -6
  69. package/dist/sig/accept.test.mjs +1 -1
  70. package/dist/sig/http.test.mjs +45 -5
  71. package/dist/sig/key.test.mjs +171 -4
  72. package/dist/sig/ld.test.mjs +59 -4
  73. package/dist/sig/mod.cjs +2 -2
  74. package/dist/sig/mod.d.cts +3 -3
  75. package/dist/sig/mod.d.ts +3 -3
  76. package/dist/sig/mod.js +2 -2
  77. package/dist/sig/owner.test.mjs +106 -7
  78. package/dist/sig/proof.test.mjs +73 -4
  79. package/dist/{std__assert-Duiq_YC9.mjs → std__assert-BUOJXi-Z.mjs} +1 -26
  80. package/dist/{temporal-G_GDghC-.mjs → temporal-BanFifxu.mjs} +1 -1
  81. package/dist/{types-m8u9L86X.cjs → types-DRPjmHVV.cjs} +101 -5
  82. package/dist/{types-DTONwNqL.js → types-miAYfiGO.js} +85 -7
  83. package/dist/utils/docloader.test.mjs +325 -5
  84. package/dist/utils/kv-cache.test.mjs +1 -1
  85. package/dist/utils/mod.cjs +1 -1
  86. package/dist/utils/mod.d.cts +1 -1
  87. package/dist/utils/mod.d.ts +1 -1
  88. package/dist/utils/mod.js +1 -1
  89. package/dist/vocab/cjs.test.mjs +1 -1
  90. package/package.json +7 -7
  91. package/dist/assert-ddO5KLpe.mjs +0 -23
  92. package/dist/docloader-B-IG1dVk.mjs +0 -52
  93. package/dist/owner-Cygg-fHW.mjs +0 -120
  94. /package/dist/{accept-Dd__NiUL.mjs → accept-BavzedPB.mjs} +0 -0
  95. /package/dist/{collection-BD6-SZ6O.mjs → collection-ChgDTHLz.mjs} +0 -0
  96. /package/dist/{keys-BAK-tUlf.mjs → keys-CtZLJq76.mjs} +0 -0
  97. /package/dist/{kv-tL2TOE9X.mjs → kv-BrZHNugx.mjs} +0 -0
  98. /package/dist/{kv-cache-FsT1ljhs.mjs → kv-cache-DqEKe5E0.mjs} +0 -0
  99. /package/dist/{negotiation-DnsfFF8I.mjs → negotiation-BehA2uul.mjs} +0 -0
  100. /package/dist/{retry-CJL0poaU.mjs → retry-B_E3V_Dx.mjs} +0 -0
  101. /package/dist/{types-DCP0WLdt.mjs → types-C37hquWI.mjs} +0 -0
@@ -52,31 +52,6 @@ import "./assert_throws-4NwKEy2q.mjs";
52
52
  throw new AssertionError(msg ?? `Expect ${actualString} >= ${expectedString}`);
53
53
  }
54
54
  //#endregion
55
- //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_greater.js
56
- /**
57
- * Make an assertion that `actual` is greater than `expected`.
58
- * If not then throw.
59
- *
60
- * @example Usage
61
- * ```ts no-eval
62
- * import { assertGreater } from "@std/assert/assert-greater";
63
- *
64
- * assertGreater(2, 1); // Doesn't throw
65
- * assertGreater(1, 1); // Throws
66
- * assertGreater(0, 1); // Throws
67
- * ```
68
- *
69
- * @typeParam T The type of the values to compare.
70
- * @param actual The actual value to compare.
71
- * @param expected The expected value to compare.
72
- * @param msg The optional message to display if the assertion fails.
73
- */ function assertGreater(actual, expected, msg) {
74
- if (actual > expected) return;
75
- const actualString = format(actual);
76
- const expectedString = format(expected);
77
- throw new AssertionError(msg ?? `Expect ${actualString} > ${expectedString}`);
78
- }
79
- //#endregion
80
55
  //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_strict_equals.js
81
56
  /**
82
57
  * Make an assertion that `actual` and `expected` are equal using
@@ -136,4 +111,4 @@ import "./assert_throws-4NwKEy2q.mjs";
136
111
  }
137
112
  }
138
113
  //#endregion
139
- export { assertExists as a, assertGreaterOrEqual as i, assertStrictEquals as n, assertGreater as r, assertStringIncludes as t };
114
+ export { assertExists as i, assertStrictEquals as n, assertGreaterOrEqual as r, assertStringIncludes as t };
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { c as getNormalizationContextLoader } from "./ld-Ch6FVq7X.mjs";
4
+ import { c as getNormalizationContextLoader } from "./ld-cfUwoi0C.mjs";
5
5
  import jsonld from "@fedify/vocab-runtime/jsonld";
6
6
  //#region src/federation/temporal.ts
7
7
  function isPlainObject(value) {
@@ -3,6 +3,80 @@ const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
4
  let _logtape_logtape = require("@logtape/logtape");
5
5
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
6
+ //#region src/utils/body.ts
7
+ /** Decoded JSON body limit: 16 MiB. @internal */
8
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
9
+ /** A body exceeded the byte limit. @internal */
10
+ var BodyTooLargeError = class extends _fedify_vocab_runtime.FetchError {
11
+ /** Creates an error for a body exceeding the given limit. */
12
+ constructor(url, maxBytes) {
13
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
14
+ this.name = "BodyTooLargeError";
15
+ }
16
+ };
17
+ /** Validates a finite, positive byte limit. @internal */
18
+ function validateBodySizeLimit(maxBytes) {
19
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
20
+ }
21
+ /**
22
+ * Reads UTF-8 text while limiting bytes received from the body stream.
23
+ * Fetch responses are already decompressed, so the stream count is authoritative.
24
+ * @param message The response or request to read.
25
+ * @param maxBytes The maximum number of decoded bytes to read.
26
+ * @param url The URL to include in errors and logs.
27
+ * @returns The decoded body text.
28
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
29
+ * @internal
30
+ */
31
+ async function readBoundedText(message, maxBytes, url) {
32
+ validateBodySizeLimit(maxBytes);
33
+ const reader = message.body?.getReader();
34
+ const tooLarge = () => {
35
+ (0, _logtape_logtape.getLogger)([
36
+ "fedify",
37
+ "runtime",
38
+ "body"
39
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
40
+ url: url.toString(),
41
+ maxBytes
42
+ });
43
+ throw new BodyTooLargeError(url, maxBytes);
44
+ };
45
+ try {
46
+ const length = message.headers.get("Content-Length");
47
+ const encoding = message.headers.get("Content-Encoding");
48
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
49
+ if (reader == null) return "";
50
+ let size = 0;
51
+ const textReader = new ReadableStream({ async pull(controller) {
52
+ const { done, value } = await reader.read();
53
+ if (done) {
54
+ controller.close();
55
+ return;
56
+ }
57
+ size += value.byteLength;
58
+ if (size > maxBytes) tooLarge();
59
+ controller.enqueue(value);
60
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
61
+ try {
62
+ const chunks = [];
63
+ while (true) {
64
+ const { done, value } = await textReader.read();
65
+ if (done) break;
66
+ chunks.push(value);
67
+ }
68
+ return chunks.join("");
69
+ } finally {
70
+ textReader.releaseLock();
71
+ }
72
+ } catch (error) {
73
+ if (reader != null) reader.cancel(error).catch(() => {});
74
+ throw error;
75
+ } finally {
76
+ reader?.releaseLock();
77
+ }
78
+ }
79
+ //#endregion
6
80
  //#region src/nodeinfo/client.ts
7
81
  const logger = (0, _logtape_logtape.getLogger)([
8
82
  "fedify",
@@ -19,8 +93,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
19
93
  * bounce the request to an internal address.
20
94
  * @param url The URL to fetch.
21
95
  * @param headers The request headers.
22
- * @returns The final response, or `null` if the URL is disallowed or there
23
- * are too many redirects.
96
+ * @returns The final response, or `null` if the URL is disallowed, its
97
+ * hostname cannot be resolved, or there are too many redirects.
24
98
  */
25
99
  async function fetchPublicUrl(url, headers) {
26
100
  let current = url instanceof URL ? url : new URL(url);
@@ -30,7 +104,11 @@ async function fetchPublicUrl(url, headers) {
30
104
  await (0, _fedify_vocab_runtime.validatePublicUrl)(current.href);
31
105
  } catch (e) {
32
106
  if (e instanceof _fedify_vocab_runtime.UrlError) {
33
- logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
107
+ if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
108
+ url: current.href,
109
+ error: e
110
+ });
111
+ else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
34
112
  url: current.href,
35
113
  error: e
36
114
  });
@@ -83,7 +161,7 @@ async function getNodeInfo(url, options = {}) {
83
161
  });
84
162
  return;
85
163
  }
86
- const wellKnownRd = await wellKnownResponse.json();
164
+ const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
87
165
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
88
166
  if (link == null || link.href == null) {
89
167
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -107,7 +185,7 @@ async function getNodeInfo(url, options = {}) {
107
185
  });
108
186
  return;
109
187
  }
110
- const data = await response.json();
188
+ const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
111
189
  if (options.parse === "none") return data;
112
190
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
113
191
  } catch (error) {
@@ -353,6 +431,18 @@ function nodeInfoToJson(nodeInfo) {
353
431
  };
354
432
  }
355
433
  //#endregion
434
+ Object.defineProperty(exports, "BodyTooLargeError", {
435
+ enumerable: true,
436
+ get: function() {
437
+ return BodyTooLargeError;
438
+ }
439
+ });
440
+ Object.defineProperty(exports, "MAX_BODY_SIZE", {
441
+ enumerable: true,
442
+ get: function() {
443
+ return MAX_BODY_SIZE;
444
+ }
445
+ });
356
446
  Object.defineProperty(exports, "getNodeInfo", {
357
447
  enumerable: true,
358
448
  get: function() {
@@ -371,3 +461,9 @@ Object.defineProperty(exports, "parseNodeInfo", {
371
461
  return parseNodeInfo;
372
462
  }
373
463
  });
464
+ Object.defineProperty(exports, "readBoundedText", {
465
+ enumerable: true,
466
+ get: function() {
467
+ return readBoundedText;
468
+ }
469
+ });
@@ -1,7 +1,81 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { getLogger } from "@logtape/logtape";
4
- import { UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
4
+ import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
5
+ //#region src/utils/body.ts
6
+ /** Decoded JSON body limit: 16 MiB. @internal */
7
+ const MAX_BODY_SIZE = 16 * 1024 * 1024;
8
+ /** A body exceeded the byte limit. @internal */
9
+ var BodyTooLargeError = class extends FetchError {
10
+ /** Creates an error for a body exceeding the given limit. */
11
+ constructor(url, maxBytes) {
12
+ super(url, `Body exceeds the limit of ${maxBytes} bytes`);
13
+ this.name = "BodyTooLargeError";
14
+ }
15
+ };
16
+ /** Validates a finite, positive byte limit. @internal */
17
+ function validateBodySizeLimit(maxBytes) {
18
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new RangeError("The body size limit must be a positive safe integer.");
19
+ }
20
+ /**
21
+ * Reads UTF-8 text while limiting bytes received from the body stream.
22
+ * Fetch responses are already decompressed, so the stream count is authoritative.
23
+ * @param message The response or request to read.
24
+ * @param maxBytes The maximum number of decoded bytes to read.
25
+ * @param url The URL to include in errors and logs.
26
+ * @returns The decoded body text.
27
+ * @throws {BodyTooLargeError} If the body exceeds the limit.
28
+ * @internal
29
+ */
30
+ async function readBoundedText(message, maxBytes, url) {
31
+ validateBodySizeLimit(maxBytes);
32
+ const reader = message.body?.getReader();
33
+ const tooLarge = () => {
34
+ getLogger([
35
+ "fedify",
36
+ "runtime",
37
+ "body"
38
+ ]).warn("Body from {url} exceeds the limit of {maxBytes} bytes.", {
39
+ url: url.toString(),
40
+ maxBytes
41
+ });
42
+ throw new BodyTooLargeError(url, maxBytes);
43
+ };
44
+ try {
45
+ const length = message.headers.get("Content-Length");
46
+ const encoding = message.headers.get("Content-Encoding");
47
+ if ((encoding == null || encoding.toLowerCase() === "identity") && length != null && /^\d+$/.test(length) && Number(length) > maxBytes) tooLarge();
48
+ if (reader == null) return "";
49
+ let size = 0;
50
+ const textReader = new ReadableStream({ async pull(controller) {
51
+ const { done, value } = await reader.read();
52
+ if (done) {
53
+ controller.close();
54
+ return;
55
+ }
56
+ size += value.byteLength;
57
+ if (size > maxBytes) tooLarge();
58
+ controller.enqueue(value);
59
+ } }, { highWaterMark: 0 }).pipeThrough(new TextDecoderStream()).getReader();
60
+ try {
61
+ const chunks = [];
62
+ while (true) {
63
+ const { done, value } = await textReader.read();
64
+ if (done) break;
65
+ chunks.push(value);
66
+ }
67
+ return chunks.join("");
68
+ } finally {
69
+ textReader.releaseLock();
70
+ }
71
+ } catch (error) {
72
+ if (reader != null) reader.cancel(error).catch(() => {});
73
+ throw error;
74
+ } finally {
75
+ reader?.releaseLock();
76
+ }
77
+ }
78
+ //#endregion
5
79
  //#region src/nodeinfo/client.ts
6
80
  const logger = getLogger([
7
81
  "fedify",
@@ -18,8 +92,8 @@ const DEFAULT_MAX_REDIRECTION = 5;
18
92
  * bounce the request to an internal address.
19
93
  * @param url The URL to fetch.
20
94
  * @param headers The request headers.
21
- * @returns The final response, or `null` if the URL is disallowed or there
22
- * are too many redirects.
95
+ * @returns The final response, or `null` if the URL is disallowed, its
96
+ * hostname cannot be resolved, or there are too many redirects.
23
97
  */
24
98
  async function fetchPublicUrl(url, headers) {
25
99
  let current = url instanceof URL ? url : new URL(url);
@@ -29,7 +103,11 @@ async function fetchPublicUrl(url, headers) {
29
103
  await validatePublicUrl(current.href);
30
104
  } catch (e) {
31
105
  if (e instanceof UrlError) {
32
- logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
106
+ if (e.reason === "dns") logger.error("DNS lookup failed for {url}: {error}", {
107
+ url: current.href,
108
+ error: e
109
+ });
110
+ else logger.error("Refused to fetch a private or invalid URL {url}: {error}", {
33
111
  url: current.href,
34
112
  error: e
35
113
  });
@@ -82,7 +160,7 @@ async function getNodeInfo(url, options = {}) {
82
160
  });
83
161
  return;
84
162
  }
85
- const wellKnownRd = await wellKnownResponse.json();
163
+ const wellKnownRd = JSON.parse(await readBoundedText(wellKnownResponse, MAX_BODY_SIZE, wellKnownUrl));
86
164
  const link = wellKnownRd?.links?.find((link) => link != null && "rel" in link && (link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.0" || link.rel === "http://nodeinfo.diaspora.software/ns/schema/2.1") && "href" in link && link.href != null);
87
165
  if (link == null || link.href == null) {
88
166
  logger.error("Failed to find a NodeInfo document link from {url}: {resourceDescriptor}", {
@@ -106,7 +184,7 @@ async function getNodeInfo(url, options = {}) {
106
184
  });
107
185
  return;
108
186
  }
109
- const data = await response.json();
187
+ const data = JSON.parse(await readBoundedText(response, MAX_BODY_SIZE, nodeInfoUrl));
110
188
  if (options.parse === "none") return data;
111
189
  return parseNodeInfo(data, { tryBestEffort: options.parse === "best-effort" }) ?? void 0;
112
190
  } catch (error) {
@@ -352,4 +430,4 @@ function nodeInfoToJson(nodeInfo) {
352
430
  };
353
431
  }
354
432
  //#endregion
355
- export { getNodeInfo as n, parseNodeInfo as r, nodeInfoToJson as t };
433
+ export { MAX_BODY_SIZE as a, BodyTooLargeError as i, getNodeInfo as n, readBoundedText as o, parseNodeInfo as r, nodeInfoToJson as t };
@@ -3,13 +3,16 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as esm_default } from "../esm-DVILvP5e.mjs";
5
5
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
6
- import "../std__assert-Duiq_YC9.mjs";
6
+ import "../std__assert-BUOJXi-Z.mjs";
7
7
  import { t as assertRejects } from "../assert_rejects-B-qJtC9Z.mjs";
8
- import { l as verifyRequest } from "../http-DIh9_s_-.mjs";
9
- import { i as rsaPrivateKey2 } from "../keys-BAK-tUlf.mjs";
10
- import { t as getAuthenticatedDocumentLoader } from "../docloader-B-IG1dVk.mjs";
8
+ import { l as verifyRequest } from "../http-B5qAeoJd.mjs";
9
+ import { i as rsaPrivateKey2 } from "../keys-CtZLJq76.mjs";
10
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-DX96uPUj.mjs";
11
11
  import { mockDocumentLoader, test } from "@fedify/fixture";
12
- import { UrlError } from "@fedify/vocab-runtime";
12
+ import { FetchError, UrlError } from "@fedify/vocab-runtime";
13
+ import { configure, reset } from "@logtape/logtape";
14
+ import dns from "node:dns/promises";
15
+ import { deepStrictEqual, ok, rejects } from "node:assert/strict";
13
16
  //#region src/utils/docloader.test.ts
14
17
  test("getAuthenticatedDocumentLoader()", async (t) => {
15
18
  esm_default.spyGlobal();
@@ -120,5 +123,322 @@ test("getAuthenticatedDocumentLoader() cancellation", {
120
123
  });
121
124
  esm_default.hardReset();
122
125
  });
126
+ test("getAuthenticatedDocumentLoader() bounds JSON after redirects", async () => {
127
+ esm_default.mockGlobal();
128
+ let oversized = true;
129
+ try {
130
+ const url = "https://example.com/bounded";
131
+ esm_default.get(url, {
132
+ status: 302,
133
+ headers: { Location: `${url}/document` }
134
+ });
135
+ esm_default.get(`${url}/document`, () => new Response("{\"name\":\"hello\"}", { headers: {
136
+ "Content-Type": "application/activity+json",
137
+ ...oversized ? { "Content-Length": String(16777217) } : {}
138
+ } }));
139
+ const loader = getAuthenticatedDocumentLoader({
140
+ privateKey: rsaPrivateKey2,
141
+ keyId: new URL("https://example.com/key2")
142
+ }, { allowPrivateAddress: true });
143
+ await assertRejects(() => loader(url), FetchError);
144
+ oversized = false;
145
+ assertEquals((await loader(url)).document, { name: "hello" });
146
+ } finally {
147
+ esm_default.hardReset();
148
+ }
149
+ });
150
+ test("getAuthenticatedDocumentLoader() logs DNS failures as such", { ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted" }, async (t) => {
151
+ const loader = getAuthenticatedDocumentLoader({
152
+ keyId: new URL("https://example.com/key2"),
153
+ privateKey: rsaPrivateKey2
154
+ });
155
+ for (const result of [
156
+ "throws",
157
+ "empty",
158
+ "private"
159
+ ]) await t.step(result, async () => {
160
+ const originalLookup = dns.lookup;
161
+ dns.lookup = (() => result === "throws" ? Promise.reject(/* @__PURE__ */ new Error("Resolver unavailable")) : Promise.resolve(result === "empty" ? [] : [{
162
+ address: "127.0.0.1",
163
+ family: 4
164
+ }]));
165
+ const records = [];
166
+ await configure({
167
+ sinks: { buffer: (record) => records.push(record) },
168
+ loggers: [{
169
+ category: "fedify",
170
+ sinks: ["buffer"],
171
+ lowestLevel: "debug"
172
+ }, {
173
+ category: ["logtape", "meta"],
174
+ sinks: []
175
+ }],
176
+ reset: true
177
+ });
178
+ try {
179
+ const url = "https://dns-failure.invalid/object";
180
+ const error = await assertRejects(() => loader(url), UrlError);
181
+ assertEquals(error.reason, result === "private" ? "disallowed" : "dns");
182
+ assertEquals(records.map((r) => [
183
+ r.level,
184
+ r.rawMessage,
185
+ r.properties.url
186
+ ]), [result === "private" ? [
187
+ "error",
188
+ "Disallowed private URL: {url}",
189
+ url
190
+ ] : [
191
+ "debug",
192
+ "DNS lookup failed for {url}",
193
+ url
194
+ ]]);
195
+ assertEquals(records[0].properties.error, error);
196
+ } finally {
197
+ await reset();
198
+ dns.lookup = originalLookup;
199
+ }
200
+ });
201
+ });
202
+ test("getAuthenticatedDocumentLoader() bounds alternate document chains", async (t) => {
203
+ const base = "https://example.com/alternate-chain/";
204
+ const loader = getAuthenticatedDocumentLoader({
205
+ keyId: new URL("https://example.com/key2"),
206
+ privateKey: rsaPrivateKey2
207
+ }, { allowPrivateAddress: true });
208
+ for (const mode of [
209
+ "header",
210
+ "html",
211
+ "mixed"
212
+ ]) for (const hops of [20, 21]) await t.step(`${mode}: ${hops} hops`, async () => {
213
+ esm_default.mockGlobal();
214
+ let requests = 0;
215
+ esm_default.get(`begin:${base}`, ({ url }) => {
216
+ requests++;
217
+ const index = Number(new URL(url).pathname.split("/").at(-1));
218
+ if (index === hops || requests > 30) return Response.json({ done: true });
219
+ const next = `${base}${index + 1}`;
220
+ if (mode === "mixed" && index % 2 === 1) return Response.redirect(next, 302);
221
+ return alternate(next, mode === "html");
222
+ });
223
+ try {
224
+ if (hops === 20) deepStrictEqual((await loader(`${base}0`)).document, { done: true });
225
+ else await rejects(loader(`${base}0`), (error) => {
226
+ ok(error instanceof FetchError);
227
+ ok(error.message.includes("Too many redirections (21)"));
228
+ return true;
229
+ });
230
+ deepStrictEqual(requests, 21);
231
+ } finally {
232
+ esm_default.hardReset();
233
+ }
234
+ });
235
+ for (const mode of [
236
+ "header",
237
+ "html",
238
+ "alternate-redirect",
239
+ "redirect-alternate",
240
+ "redirect-intermediate"
241
+ ]) await t.step(`${mode}: cycle`, async () => {
242
+ esm_default.mockGlobal();
243
+ let requests = 0;
244
+ esm_default.get(`begin:${base}`, ({ url }) => {
245
+ requests++;
246
+ if (requests > 30) return Response.json({ stopped: true });
247
+ const index = Number(new URL(url).pathname.split("/").at(-1));
248
+ const next = `${base}${mode === "redirect-intermediate" ? index === 0 ? 1 : index === 1 ? 2 : 1 : 1 - index}`;
249
+ if (mode === "alternate-redirect" && index === 1 || (mode === "redirect-alternate" || mode === "redirect-intermediate") && index === 0) return Response.redirect(next, 302);
250
+ return alternate(next, mode === "html");
251
+ });
252
+ try {
253
+ await rejects(loader(`${base}0`), (error) => {
254
+ ok(error instanceof FetchError);
255
+ ok(error.message.includes("Redirect loop detected:"));
256
+ return true;
257
+ });
258
+ deepStrictEqual(requests, mode === "redirect-intermediate" ? 3 : 2);
259
+ } finally {
260
+ esm_default.hardReset();
261
+ }
262
+ });
263
+ await t.step("alternate followed by 20 redirects shares the limit", async () => {
264
+ esm_default.mockGlobal();
265
+ let requests = 0;
266
+ esm_default.get(`begin:${base}`, ({ url }) => {
267
+ requests++;
268
+ const index = Number(new URL(url).pathname.split("/").at(-1));
269
+ if (index === 21) return Response.json({ done: true });
270
+ return index === 0 ? alternate(`${base}1`, false) : Response.redirect(`${base}${index + 1}`, 302);
271
+ });
272
+ try {
273
+ await rejects(loader(`${base}0`), (error) => {
274
+ ok(error instanceof FetchError);
275
+ ok(error.message.includes("Too many redirections (21)"));
276
+ return true;
277
+ });
278
+ deepStrictEqual(requests, 21);
279
+ } finally {
280
+ esm_default.hardReset();
281
+ }
282
+ });
283
+ await t.step("relative alternate after redirect and isolated calls", async () => {
284
+ esm_default.mockGlobal();
285
+ esm_default.get(`${base}start`, Response.redirect(`${base}html`, 302));
286
+ esm_default.get(`${base}html`, alternate("./document", true));
287
+ esm_default.get(`${base}document`, Response.json({ done: true }));
288
+ try {
289
+ for (let i = 0; i < 2; i++) {
290
+ const results = await Promise.all([loader(`${base}start`), loader(`${base}start`)]);
291
+ for (const result of results) {
292
+ deepStrictEqual(result.document, { done: true });
293
+ deepStrictEqual(result.documentUrl, `${base}document`);
294
+ }
295
+ }
296
+ } finally {
297
+ esm_default.hardReset();
298
+ }
299
+ });
300
+ function alternate(next, html) {
301
+ return html ? new Response(`<link rel="alternate" type="application/activity+json" href="${next}">`, { headers: { "Content-Type": "text/html" } }) : new Response("not JSON", { headers: {
302
+ "Content-Type": "text/plain",
303
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
304
+ } });
305
+ }
306
+ });
307
+ test("getAuthenticatedDocumentLoader() preserves cancellation across alternates", async (t) => {
308
+ const base = "https://example.com/alternate-abort/";
309
+ const loader = getAuthenticatedDocumentLoader({
310
+ keyId: new URL("https://example.com/key2"),
311
+ privateKey: rsaPrivateKey2
312
+ }, { allowPrivateAddress: true });
313
+ for (const html of [false, true]) await t.step(html ? "HTML" : "Link header", async () => {
314
+ esm_default.mockGlobal();
315
+ const controller = new AbortController();
316
+ const reason = /* @__PURE__ */ new Error("Alternate document loading cancelled");
317
+ let requests = 0;
318
+ esm_default.get(`begin:${base}`, ({ url }) => {
319
+ requests++;
320
+ const index = Number(new URL(url).pathname.split("/").at(-1));
321
+ if (index === 2) return Response.json({ done: true });
322
+ if (index === 1) controller.abort(reason);
323
+ const next = `${base}${index + 1}`;
324
+ return new Response(html ? `<link rel="alternate" type="application/activity+json" href="${next}">` : "not JSON", { headers: html ? { "Content-Type": "text/html" } : {
325
+ "Content-Type": "text/plain",
326
+ Link: `<${next}>; rel="alternate"; type="application/activity+json"`
327
+ } });
328
+ });
329
+ try {
330
+ await rejects(loader(`${base}0`, { signal: controller.signal }), (error) => {
331
+ ok(error === reason);
332
+ return true;
333
+ });
334
+ deepStrictEqual(requests, 2);
335
+ } finally {
336
+ esm_default.hardReset();
337
+ }
338
+ });
339
+ });
340
+ test("getAuthenticatedDocumentLoader() tracks fallback redirects after alternates", async () => {
341
+ esm_default.mockGlobal();
342
+ let requests = 0;
343
+ let knocks = 0;
344
+ const first = "https://example.com/fallback-alternate/a";
345
+ const second = "https://example.com/fallback-alternate/b";
346
+ esm_default.get(first, () => {
347
+ requests++;
348
+ if (requests > 30) return Response.json({ stopped: true });
349
+ return new Response("", { headers: {
350
+ "Content-Type": "text/plain",
351
+ Link: `<${second}>; rel="alternate"; type="application/activity+json"`
352
+ } });
353
+ });
354
+ esm_default.get(second, () => {
355
+ requests++;
356
+ return ++knocks % 2 === 1 ? new Response("", { status: 401 }) : Response.redirect(first, 302);
357
+ });
358
+ try {
359
+ const loader = getAuthenticatedDocumentLoader({
360
+ keyId: new URL("https://example.com/key2"),
361
+ privateKey: rsaPrivateKey2
362
+ }, { allowPrivateAddress: true });
363
+ await assertRejects(() => loader(first), FetchError, "Redirect loop detected:");
364
+ assertEquals(requests, 3);
365
+ } finally {
366
+ esm_default.hardReset();
367
+ }
368
+ });
369
+ test("getAuthenticatedDocumentLoader() resolves relative alternates without response URLs", async () => {
370
+ const originalFetch = globalThis.fetch;
371
+ const base = "https://example.com/empty-response-url/";
372
+ const urls = [];
373
+ globalThis.fetch = (input) => {
374
+ const url = input instanceof Request ? input.url : String(input);
375
+ urls.push(url);
376
+ if (url === `${base}start`) return Promise.resolve(Response.redirect(`${base}nested/page`, 302));
377
+ if (url === `${base}nested/page`) return Promise.resolve(new Response("<link rel=\"alternate\" type=\"application/activity+json\" href=\"document\">", { headers: { "Content-Type": "text/html" } }));
378
+ assertEquals(url, `${base}nested/document`);
379
+ return Promise.resolve(Response.json({ done: true }));
380
+ };
381
+ try {
382
+ assertEquals(await getAuthenticatedDocumentLoader({
383
+ keyId: new URL("https://example.com/key2"),
384
+ privateKey: rsaPrivateKey2
385
+ }, { allowPrivateAddress: true })(`${base}start`), {
386
+ contextUrl: null,
387
+ document: { done: true },
388
+ documentUrl: `${base}nested/document`
389
+ });
390
+ assertEquals(urls, [
391
+ `${base}start`,
392
+ `${base}nested/page`,
393
+ `${base}nested/document`
394
+ ]);
395
+ } finally {
396
+ globalThis.fetch = originalFetch;
397
+ }
398
+ });
399
+ test("getAuthenticatedDocumentLoader() validates alternate addresses", async () => {
400
+ esm_default.mockGlobal();
401
+ let privateRequests = 0;
402
+ const url = "https://8.8.8.8/alternate";
403
+ esm_default.get(url, new Response("", { headers: {
404
+ "Content-Type": "text/plain",
405
+ Link: "<http://127.0.0.1/private>; rel=\"alternate\"; type=\"application/activity+json\""
406
+ } }));
407
+ esm_default.get("http://127.0.0.1/private", () => {
408
+ privateRequests++;
409
+ return Response.json({ private: true });
410
+ });
411
+ try {
412
+ const loader = getAuthenticatedDocumentLoader({
413
+ keyId: new URL("https://example.com/key2"),
414
+ privateKey: rsaPrivateKey2
415
+ });
416
+ await assertRejects(() => loader(url), UrlError);
417
+ assertEquals(privateRequests, 0);
418
+ } finally {
419
+ esm_default.hardReset();
420
+ }
421
+ });
422
+ test("getAuthenticatedDocumentLoader() rejects cancellation before fetching", async () => {
423
+ esm_default.mockGlobal();
424
+ let requests = 0;
425
+ const url = "https://example.com/pre-aborted-alternate";
426
+ esm_default.get(url, () => {
427
+ requests++;
428
+ return Response.json({ done: true });
429
+ });
430
+ try {
431
+ const loader = getAuthenticatedDocumentLoader({
432
+ keyId: new URL("https://example.com/key2"),
433
+ privateKey: rsaPrivateKey2
434
+ }, { allowPrivateAddress: true });
435
+ const controller = new AbortController();
436
+ controller.abort();
437
+ await rejects(loader(url, { signal: controller.signal }), { name: "AbortError" });
438
+ deepStrictEqual(requests, 0);
439
+ } finally {
440
+ esm_default.hardReset();
441
+ }
442
+ });
123
443
  //#endregion
124
444
  export {};
@@ -1,7 +1,7 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as kvCache, t as MockKvStore } from "../kv-cache-FsT1ljhs.mjs";
4
+ import { n as kvCache, t as MockKvStore } from "../kv-cache-DqEKe5E0.mjs";
5
5
  import { mockDocumentLoader, test } from "@fedify/fixture";
6
6
  import { deepStrictEqual, throws } from "node:assert";
7
7
  import { preloadedContexts } from "@fedify/vocab-runtime";