@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
@@ -34,4 +34,22 @@ import { l as AssertionError } from "./assert_equals-Ew3jOFa3.mjs";
34
34
  throw new AssertionError(msg);
35
35
  }
36
36
  //#endregion
37
- export { assertInstanceOf as t };
37
+ //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert.js
38
+ /**
39
+ * Make an assertion, error will be thrown if `expr` does not have truthy value.
40
+ *
41
+ * @example Usage
42
+ * ```ts no-eval
43
+ * import { assert } from "@std/assert/assert";
44
+ *
45
+ * assert("hello".includes("ello")); // Doesn't throw
46
+ * assert("hello".includes("world")); // Throws
47
+ * ```
48
+ *
49
+ * @param expr The expression to test.
50
+ * @param msg The optional message to display if the assertion fails.
51
+ */ function assert(expr, msg = "") {
52
+ if (!expr) throw new AssertionError(msg);
53
+ }
54
+ //#endregion
55
+ export { assertInstanceOf as n, assert as t };
@@ -2,8 +2,8 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as RouterError, t as Router } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-B_Gwkjh7.mjs";
6
- import { t as InboxListenerSet } from "./inbox-BxCHIWPD.mjs";
5
+ import { n as version, t as name } from "./deno-9AaebKOA.mjs";
6
+ import { t as InboxListenerSet } from "./inbox-BSyjaFbo.mjs";
7
7
  import { getTypeId } from "@fedify/vocab";
8
8
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
9
9
  import { getLogger } from "@logtape/logtape";
@@ -43,7 +43,7 @@ var FederationBuilderImpl = class {
43
43
  this.collectionTypeIds = {};
44
44
  }
45
45
  async build(options) {
46
- const { FederationImpl } = await import("./middleware-DABFg_Xy.mjs");
46
+ const { FederationImpl } = await import("./middleware-DDT9ljTQ.mjs");
47
47
  const f = new FederationImpl(options);
48
48
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
49
49
  f.router = this.router.clone();
@@ -1,8 +1,82 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
4
+ import { FetchError, UrlError, getUserAgent, validatePublicUrl } from "@fedify/vocab-runtime";
5
5
  import { getLogger } from "@logtape/logtape";
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 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
+ 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 = 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 validatePublicUrl(current.href);
31
105
  } catch (e) {
32
106
  if (e instanceof 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) {
@@ -311,4 +389,4 @@ function parseUsage(data, options = {}) {
311
389
  };
312
390
  }
313
391
  //#endregion
314
- export { parseProtocol as a, parseUsage as c, parseOutboundService as i, parseInboundService as n, parseServices as o, parseNodeInfo as r, parseSoftware as s, getNodeInfo as t };
392
+ export { parseProtocol as a, parseUsage as c, readBoundedText as d, parseOutboundService as i, BodyTooLargeError as l, parseInboundService as n, parseServices as o, parseNodeInfo as r, parseSoftware as s, getNodeInfo as t, MAX_BODY_SIZE as u };
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ct as ActivityTransformer, n as Context } from "../context-o22vjzS2.cjs";
2
+ import { Ct as ActivityTransformer, n as Context } from "../context-D4s0Wk3N.cjs";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { Ct as ActivityTransformer, n as Context } from "../context-_l9t359k.js";
2
+ import { Ct as ActivityTransformer, n as Context } from "../context-BgU5UWTx.js";
3
3
  import { Activity } from "@fedify/vocab";
4
4
 
5
5
  //#region src/compat/transformers.d.ts
@@ -2,10 +2,9 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
6
- import { t as assert } from "../assert-ddO5KLpe.mjs";
7
- import { t as MemoryKvStore } from "../kv-tL2TOE9X.mjs";
8
- import { _ as autoIdAssigner, g as actorDehydrator, n as FederationImpl } from "../middleware-D2O8b03q.mjs";
5
+ import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
6
+ import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
7
+ import { _ as autoIdAssigner, g as actorDehydrator, n as FederationImpl } from "../middleware-DHHRwSZH.mjs";
9
8
  import { test } from "@fedify/fixture";
10
9
  import { Follow, Person } from "@fedify/vocab";
11
10
  //#region src/compat/transformers.test.ts
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CSddvgWN.js";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-BDZeS5om.js";
4
- import { n as GetKeyOwnerOptions } from "./owner-CnngXDNJ.js";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-Dl00HXr4.js";
4
+ import { n as GetKeyOwnerOptions } from "./owner-CiBPcC6F.js";
5
5
  import { n as KvStore, t as KvKey } from "./kv-D6hNiMTK.js";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, TraverseCollectionOptions } from "@fedify/vocab";
7
7
  import { Span, Tracer, TracerProvider } from "@opentelemetry/api";
@@ -1454,7 +1454,8 @@ interface FederationOptions<TContextData> {
1454
1454
  */
1455
1455
  authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
1456
1456
  /**
1457
- * Whether to allow fetching private network addresses in the document loader.
1457
+ * Whether to allow private network addresses in the document loader and
1458
+ * outbound activity delivery, including redirects.
1458
1459
  *
1459
1460
  * If turned on, {@link FederationOptions.documentLoader},
1460
1461
  * {@link FederationOptions.contextLoader}, and
@@ -1,7 +1,7 @@
1
1
  /// <reference lib="esnext.temporal" />
2
2
  import { o as JsonValue, s as NodeInfo, t as GetNodeInfoOptions } from "./client-CAM_bQXx.cjs";
3
- import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-C87EWkO0.cjs";
4
- import { n as GetKeyOwnerOptions } from "./owner-DEvZuyOE.cjs";
3
+ import { o as VerifyRequestFailureReason, t as HttpMessageSignaturesSpec } from "./http-DqZhyEso.cjs";
4
+ import { n as GetKeyOwnerOptions } from "./owner-B3hNQCG8.cjs";
5
5
  import { n as KvStore, t as KvKey } from "./kv-gJ8LYbxX.cjs";
6
6
  import { Activity, Actor, Collection, CryptographicKey, Hashtag, Link, LookupObjectOptions, Multikey, Object as Object$1, Recipient, TraverseCollectionOptions } from "@fedify/vocab";
7
7
  import { AuthenticatedDocumentLoaderFactory, DocumentLoader, DocumentLoaderFactory, GetUserAgentOptions } from "@fedify/vocab-runtime";
@@ -1454,7 +1454,8 @@ interface FederationOptions<TContextData> {
1454
1454
  */
1455
1455
  authenticatedDocumentLoaderFactory?: AuthenticatedDocumentLoaderFactory;
1456
1456
  /**
1457
- * Whether to allow fetching private network addresses in the document loader.
1457
+ * Whether to allow private network addresses in the document loader and
1458
+ * outbound activity delivery, including redirects.
1458
1459
  *
1459
1460
  * If turned on, {@link FederationOptions.documentLoader},
1460
1461
  * {@link FederationOptions.contextLoader}, and
@@ -3,6 +3,6 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  //#region deno.json
5
5
  var name = "@fedify/fedify";
6
- var version = "2.1.23";
6
+ var version = "2.1.25";
7
7
  //#endregion
8
8
  export { version as n, name as t };
@@ -0,0 +1,78 @@
1
+ import "@js-temporal/polyfill";
2
+ import "urlpattern-polyfill";
3
+ globalThis.addEventListener = () => {};
4
+ import { s as validateCryptoKey } from "./key-x9f-H7eO.mjs";
5
+ import { n as doubleKnock } from "./http-B5qAeoJd.mjs";
6
+ import { curry } from "es-toolkit";
7
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, validatePublicUrl } from "@fedify/vocab-runtime";
8
+ import { getLogger } from "@logtape/logtape";
9
+ //#region src/utils/docloader.ts
10
+ const logger = getLogger([
11
+ "fedify",
12
+ "utils",
13
+ "docloader"
14
+ ]);
15
+ const DEFAULT_MAX_REDIRECTION = 20;
16
+ /**
17
+ * Gets an authenticated {@link DocumentLoader} for the given identity.
18
+ * Note that an authenticated document loader intentionally does not cache
19
+ * the fetched documents.
20
+ * At most 20 HTTP redirects and alternate document links are followed in total
21
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
22
+ * @param identity The identity to get the document loader for.
23
+ * The actor's key pair.
24
+ * @param options The options for the document loader.
25
+ * @returns The authenticated document loader.
26
+ * @throws {TypeError} If the key is invalid or unsupported.
27
+ * @since 0.4.0
28
+ */
29
+ function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
30
+ validateCryptoKey(identity.privateKey);
31
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
32
+ options?.signal?.throwIfAborted();
33
+ let currentUrl = new URL(url).href;
34
+ await validateUrl(currentUrl);
35
+ visited.add(currentUrl);
36
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
37
+ function follow(nextUrl) {
38
+ options?.signal?.throwIfAborted();
39
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
40
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
41
+ redirected++;
42
+ }
43
+ const response = await doubleKnock(originalRequest, identity, {
44
+ specDeterminer,
45
+ log: curry(logRequest)(logger),
46
+ tracerProvider,
47
+ signal: options?.signal,
48
+ validateRedirect: async (nextUrl) => {
49
+ follow(nextUrl);
50
+ await validateUrl(nextUrl);
51
+ visited.add(nextUrl);
52
+ currentUrl = nextUrl;
53
+ }
54
+ });
55
+ return getRemoteDocument(currentUrl, response, (alternateUrl) => {
56
+ follow(alternateUrl);
57
+ return load(alternateUrl, options, redirected, visited);
58
+ });
59
+ }
60
+ async function validateUrl(url) {
61
+ if (!allowPrivateAddress) try {
62
+ await validatePublicUrl(url);
63
+ } catch (error) {
64
+ if (error instanceof UrlError) if (error.reason === "dns") logger.debug("DNS lookup failed for {url}", {
65
+ url,
66
+ error
67
+ });
68
+ else logger.error("Disallowed private URL: {url}", {
69
+ url,
70
+ error
71
+ });
72
+ throw error;
73
+ }
74
+ }
75
+ return (url, options) => load(url, options);
76
+ }
77
+ //#endregion
78
+ export { getAuthenticatedDocumentLoader as t };
@@ -2,10 +2,10 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import { a as assertExists } from "../std__assert-Duiq_YC9.mjs";
5
+ import { i as assertExists } from "../std__assert-BUOJXi-Z.mjs";
6
6
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
7
- import { t as MemoryKvStore } from "../kv-tL2TOE9X.mjs";
8
- import { n as createFederationBuilder } from "../builder-BlCsjkrX.mjs";
7
+ import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
8
+ import { n as createFederationBuilder } from "../builder-DsBAef6b.mjs";
9
9
  import { test } from "@fedify/fixture";
10
10
  import { Activity, Note, Person } from "@fedify/vocab";
11
11
  //#region src/federation/builder.test.ts
@@ -2,8 +2,8 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
6
- import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-BD6-SZ6O.mjs";
5
+ import "../std__assert-BUOJXi-Z.mjs";
6
+ import { n as digest, t as buildCollectionSynchronizationHeader } from "../collection-ChgDTHLz.mjs";
7
7
  import { test } from "@fedify/fixture";
8
8
  import { decodeHex } from "byte-encodings/hex";
9
9
  //#region src/federation/collection.test.ts
@@ -3,18 +3,20 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as createRequestContext, t as createInboxContext } from "../context-Juj6bdHC.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 { t as assert } from "../assert-ddO5KLpe.mjs";
9
- import { r as parseAcceptSignature } from "../accept-Dd__NiUL.mjs";
10
- import { s as signRequest } from "../http-DIh9_s_-.mjs";
11
- import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-BAK-tUlf.mjs";
12
- import { a as compactJsonLd, f as signJsonLd } from "../ld-Ch6FVq7X.mjs";
13
- import { t as MemoryKvStore } from "../kv-tL2TOE9X.mjs";
14
- import { a as createFederation, c as handleCollection, d as handleObject, f as respondWithObject, l as handleCustomCollection, p as respondWithObjectIfAcceptable, s as handleActor, u as handleInbox } from "../middleware-D2O8b03q.mjs";
15
- import { t as InboxListenerSet } from "../inbox-BxCHIWPD.mjs";
8
+ import { t as assert } from "../assert-BhQd8QHP.mjs";
9
+ import { r as parseAcceptSignature } from "../accept-BavzedPB.mjs";
10
+ import { a as generateCryptoKeyPair } from "../key-x9f-H7eO.mjs";
11
+ import { s as signRequest } from "../http-B5qAeoJd.mjs";
12
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, s as rsaPublicKey2 } from "../keys-CtZLJq76.mjs";
13
+ import { a as compactJsonLd, f as signJsonLd } from "../ld-cfUwoi0C.mjs";
14
+ import { n as signObject } from "../proof-CI4rsVLu.mjs";
15
+ import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
16
+ import { a as createFederation, c as handleCollection, d as handleObject, f as respondWithObject, l as handleCustomCollection, p as respondWithObjectIfAcceptable, s as handleActor, u as handleInbox } from "../middleware-DHHRwSZH.mjs";
17
+ import { t as InboxListenerSet } from "../inbox-BSyjaFbo.mjs";
16
18
  import { createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
17
- import { Create, Note, Person } from "@fedify/vocab";
19
+ import { Create, CryptographicKey, Multikey, Note, Person } from "@fedify/vocab";
18
20
  import { FetchError } from "@fedify/vocab-runtime";
19
21
  //#region src/federation/handler.test.ts
20
22
  test("handleActor()", async () => {
@@ -3705,5 +3707,121 @@ test("handleInbox() challenge policy enabled + unverifiedActivityHandler throws
3705
3707
  assertEquals(response.headers.get("Cache-Control"), "no-store", "Cache-Control: no-store must be set for challenge-response");
3706
3708
  assertEquals(response.headers.get("Vary"), "Accept, Signature", "Vary header must include Accept and Signature");
3707
3709
  });
3710
+ test("handleInbox() rejects forged key ownership", async () => {
3711
+ const impersonated = "https://example.com/person2";
3712
+ const attackerKeyId = new URL("https://attacker.example/key");
3713
+ const attackerMultikeyId = new URL("https://attacker.example/multikey");
3714
+ const { privateKey, publicKey } = await generateCryptoKeyPair("RSASSA-PKCS1-v1_5");
3715
+ const forgedKeyDocument = await new CryptographicKey({
3716
+ id: attackerKeyId,
3717
+ owner: new URL(impersonated),
3718
+ publicKey
3719
+ }).toJsonLd({ contextLoader: mockDocumentLoader });
3720
+ const ed25519 = await generateCryptoKeyPair("Ed25519");
3721
+ const forgedMultikeyDocument = await new Multikey({
3722
+ id: attackerMultikeyId,
3723
+ controller: new URL(impersonated),
3724
+ publicKey: ed25519.publicKey
3725
+ }).toJsonLd({ contextLoader: mockDocumentLoader });
3726
+ const documentLoader = (resource) => {
3727
+ if (resource === attackerKeyId.href) return Promise.resolve({
3728
+ contextUrl: null,
3729
+ documentUrl: resource,
3730
+ document: forgedKeyDocument
3731
+ });
3732
+ if (resource === attackerMultikeyId.href) return Promise.resolve({
3733
+ contextUrl: null,
3734
+ documentUrl: resource,
3735
+ document: forgedMultikeyDocument
3736
+ });
3737
+ return mockDocumentLoader(resource);
3738
+ };
3739
+ const federation = createFederation({ kv: new MemoryKvStore() });
3740
+ const inboxOptions = {
3741
+ kv: new MemoryKvStore(),
3742
+ kvPrefixes: {
3743
+ activityIdempotence: ["_fedify", "activityIdempotence"],
3744
+ publicKey: ["_fedify", "publicKey"],
3745
+ acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"]
3746
+ },
3747
+ actorDispatcher: ((_ctx, identifier) => identifier === "someone" ? new Person({ name: "Someone" }) : null),
3748
+ inboxListeners: new InboxListenerSet(),
3749
+ onNotFound: () => new Response("Not found", { status: 404 }),
3750
+ signatureTimeWindow: { minutes: 5 },
3751
+ skipSignatureVerification: false
3752
+ };
3753
+ const handle = async (request) => {
3754
+ const context = createRequestContext({
3755
+ federation,
3756
+ request,
3757
+ url: new URL(request.url),
3758
+ data: void 0,
3759
+ documentLoader,
3760
+ contextLoader: mockDocumentLoader
3761
+ });
3762
+ return await handleInbox(request, {
3763
+ recipient: null,
3764
+ context,
3765
+ inboxContextFactory(_activity) {
3766
+ return createInboxContext({
3767
+ ...context,
3768
+ clone: void 0
3769
+ });
3770
+ },
3771
+ ...inboxOptions
3772
+ });
3773
+ };
3774
+ const activity = {
3775
+ "@context": [
3776
+ "https://www.w3.org/ns/activitystreams",
3777
+ "https://w3id.org/identity/v1",
3778
+ "https://w3id.org/security/v1",
3779
+ "https://w3id.org/security/data-integrity/v1"
3780
+ ],
3781
+ id: "https://attacker.example/activities/1",
3782
+ type: "Create",
3783
+ actor: impersonated,
3784
+ object: {
3785
+ id: "https://attacker.example/notes/1",
3786
+ type: "Note",
3787
+ attributedTo: impersonated,
3788
+ content: "Hello World!"
3789
+ }
3790
+ };
3791
+ const httpSignedResponse = await handle(await signRequest(new Request("https://example.com/", {
3792
+ method: "POST",
3793
+ body: JSON.stringify(activity)
3794
+ }), privateKey, attackerKeyId));
3795
+ const ldSignedResponse = await handle(new Request("https://example.com/", {
3796
+ method: "POST",
3797
+ body: JSON.stringify(await signJsonLd(activity, privateKey, attackerKeyId, { contextLoader: mockDocumentLoader }))
3798
+ }));
3799
+ const signedObject = await signObject(await Create.fromJsonLd(activity, {
3800
+ documentLoader,
3801
+ contextLoader: mockDocumentLoader
3802
+ }), ed25519.privateKey, attackerMultikeyId, {
3803
+ documentLoader,
3804
+ contextLoader: mockDocumentLoader,
3805
+ context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
3806
+ });
3807
+ const proofSignedResponse = await handle(new Request("https://example.com/", {
3808
+ method: "POST",
3809
+ body: JSON.stringify(await signedObject.toJsonLd({
3810
+ format: "compact",
3811
+ contextLoader: mockDocumentLoader,
3812
+ context: ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/data-integrity/v1"]
3813
+ }))
3814
+ }));
3815
+ assertEquals([
3816
+ httpSignedResponse.status,
3817
+ ldSignedResponse.status,
3818
+ proofSignedResponse.status
3819
+ ], [
3820
+ 401,
3821
+ 401,
3822
+ 401
3823
+ ]);
3824
+ assertEquals(await httpSignedResponse.text(), "Failed to verify the request signature.");
3825
+ });
3708
3826
  //#endregion
3709
3827
  export {};
@@ -2,11 +2,11 @@ import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import "../std__assert-Duiq_YC9.mjs";
6
- import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-BAK-tUlf.mjs";
7
- import { n as signObject } from "../proof-Dt5GV-xi.mjs";
8
- import { t as MemoryKvStore } from "../kv-tL2TOE9X.mjs";
9
- import { a as createFederation } from "../middleware-D2O8b03q.mjs";
5
+ import "../std__assert-BUOJXi-Z.mjs";
6
+ import { n as ed25519PrivateKey, r as ed25519PublicKey, t as ed25519Multikey } from "../keys-CtZLJq76.mjs";
7
+ import { n as signObject } from "../proof-CI4rsVLu.mjs";
8
+ import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
9
+ import { a as createFederation } from "../middleware-DHHRwSZH.mjs";
10
10
  import { mockDocumentLoader, test } from "@fedify/fixture";
11
11
  import { Create, Follow, Person } from "@fedify/vocab";
12
12
  //#region src/federation/idempotency.test.ts
@@ -3,7 +3,7 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
5
  import { t as assertThrows } from "../assert_throws-4NwKEy2q.mjs";
6
- import { t as InboxListenerSet } from "../inbox-BxCHIWPD.mjs";
6
+ import { t as InboxListenerSet } from "../inbox-BSyjaFbo.mjs";
7
7
  import { test } from "@fedify/fixture";
8
8
  import { Activity, Create, Invite, Offer, Update } from "@fedify/vocab";
9
9
  //#region src/federation/inbox.test.ts
@@ -2,10 +2,9 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { t as assertEquals } from "../assert_equals-Ew3jOFa3.mjs";
5
- import { t as assertInstanceOf } from "../assert_instance_of-C4Ri6VuN.mjs";
6
- import { t as assert } from "../assert-ddO5KLpe.mjs";
7
- import { t as MemoryKvStore } from "../kv-tL2TOE9X.mjs";
8
- import { t as KvKeyCache } from "../keycache-CCSwkQcY.mjs";
5
+ import { n as assertInstanceOf, t as assert } from "../assert-BhQd8QHP.mjs";
6
+ import { t as MemoryKvStore } from "../kv-BrZHNugx.mjs";
7
+ import { t as KvKeyCache } from "../keycache-CaO7wyQA.mjs";
9
8
  import { test } from "@fedify/fixture";
10
9
  import { CryptographicKey, Multikey } from "@fedify/vocab";
11
10
  //#region src/federation/keycache.test.ts
@@ -13,25 +12,41 @@ test("KvKeyCache.set()", async () => {
13
12
  const kv = new MemoryKvStore();
14
13
  const cache = new KvKeyCache(kv, ["pk"]);
15
14
  await cache.set(new URL("https://example.com/key"), new CryptographicKey({ id: new URL("https://example.com/key") }));
16
- assertEquals(await kv.get(["pk", "https://example.com/key"]), {
15
+ assertEquals(await kv.get([
16
+ "pk",
17
+ "2",
18
+ "https://example.com/key"
19
+ ]), {
17
20
  "@context": "https://w3id.org/security/v1",
18
21
  id: "https://example.com/key",
19
22
  type: "CryptographicKey"
20
23
  });
21
24
  await cache.set(new URL("https://example.com/key2"), new Multikey({ id: new URL("https://example.com/key2") }));
22
- assertEquals(await kv.get(["pk", "https://example.com/key2"]), {
25
+ assertEquals(await kv.get([
26
+ "pk",
27
+ "2",
28
+ "https://example.com/key2"
29
+ ]), {
23
30
  "@context": "https://w3id.org/security/multikey/v1",
24
31
  id: "https://example.com/key2",
25
32
  type: "Multikey"
26
33
  });
27
34
  await cache.set(new URL("https://example.com/null"), null);
28
35
  assert(cache.nullKeys.has("https://example.com/null"));
29
- assertEquals(await kv.get(["pk", "https://example.com/null"]), null);
36
+ assertEquals(await kv.get([
37
+ "pk",
38
+ "2",
39
+ "https://example.com/null"
40
+ ]), null);
30
41
  });
31
42
  test("KvKeyCache.get()", async () => {
32
43
  const kv = new MemoryKvStore();
33
44
  const cache = new KvKeyCache(kv, ["pk"]);
34
- await kv.set(["pk", "https://example.com/key"], {
45
+ await kv.set([
46
+ "pk",
47
+ "2",
48
+ "https://example.com/key"
49
+ ], {
35
50
  "@context": "https://w3id.org/security/v1",
36
51
  id: "https://example.com/key",
37
52
  type: "CryptographicKey"
@@ -39,7 +54,11 @@ test("KvKeyCache.get()", async () => {
39
54
  const cryptoKey = await cache.get(new URL("https://example.com/key"));
40
55
  assertInstanceOf(cryptoKey, CryptographicKey);
41
56
  assertEquals(cryptoKey?.id?.href, "https://example.com/key");
42
- await kv.set(["pk", "https://example.com/key2"], {
57
+ await kv.set([
58
+ "pk",
59
+ "2",
60
+ "https://example.com/key2"
61
+ ], {
43
62
  "@context": "https://w3id.org/security/multikey/v1",
44
63
  id: "https://example.com/key2",
45
64
  type: "Multikey"
@@ -49,7 +68,11 @@ test("KvKeyCache.get()", async () => {
49
68
  assertEquals(multikey?.id?.href, "https://example.com/key2");
50
69
  cache.nullKeys.set("https://example.com/null", Temporal.Now.instant().add(Temporal.Duration.from({ minutes: 10 })));
51
70
  assertEquals(await cache.get(new URL("https://example.com/null")), null);
52
- await kv.set(["pk", "https://example.com/null2"], null);
71
+ await kv.set([
72
+ "pk",
73
+ "2",
74
+ "https://example.com/null2"
75
+ ], null);
53
76
  assertEquals(await new KvKeyCache(kv, ["pk"]).get(new URL("https://example.com/null2")), null);
54
77
  });
55
78
  test("KvKeyCache fetch error metadata", async () => {
@@ -90,5 +113,16 @@ test("KvKeyCache unavailable entries expire", async () => {
90
113
  assertEquals(await cache.get(keyId), void 0);
91
114
  assertEquals(await cache.getFetchError(keyId), void 0);
92
115
  });
116
+ test("KvKeyCache.get() ignores entries from an earlier generation", async () => {
117
+ const kv = new MemoryKvStore();
118
+ const cache = new KvKeyCache(kv, ["pk"]);
119
+ await kv.set(["pk", "https://example.com/key"], {
120
+ "@context": "https://w3id.org/security/v1",
121
+ id: "https://example.com/key",
122
+ owner: "https://example.com/impersonated",
123
+ type: "CryptographicKey"
124
+ });
125
+ assertEquals(await cache.get(new URL("https://example.com/key")), void 0);
126
+ });
93
127
  //#endregion
94
128
  export {};