@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
@@ -1,19 +1,22 @@
1
1
  import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
- import { d as validateCryptoKey, t as doubleKnock } from "./http-88deUx3Y.js";
3
+ import { f as validateCryptoKey, t as doubleKnock } from "./http-CdJ6n4rA.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
- import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
6
+ import { FetchError, UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
7
7
  //#region src/utils/docloader.ts
8
8
  const logger$1 = getLogger([
9
9
  "fedify",
10
10
  "utils",
11
11
  "docloader"
12
12
  ]);
13
+ const DEFAULT_MAX_REDIRECTION = 20;
13
14
  /**
14
15
  * Gets an authenticated {@link DocumentLoader} for the given identity.
15
16
  * Note that an authenticated document loader intentionally does not cache
16
17
  * the fetched documents.
18
+ * At most 20 HTTP redirects and alternate document links are followed in total
19
+ * per call. Revisiting a URL within that chain throws a {@link FetchError}.
17
20
  * @param identity The identity to get the document loader for.
18
21
  * The actor's key pair.
19
22
  * @param options The options for the document loader.
@@ -23,28 +26,51 @@ const logger$1 = getLogger([
23
26
  */
24
27
  function getAuthenticatedDocumentLoader(identity, { allowPrivateAddress, userAgent, specDeterminer, tracerProvider } = {}) {
25
28
  validateCryptoKey(identity.privateKey);
26
- async function load(url, options) {
27
- await validateUrl(url);
28
- return getRemoteDocument(url, await doubleKnock(createActivityPubRequest(url, { userAgent }), identity, {
29
+ async function load(url, options, redirected = 0, visited = /* @__PURE__ */ new Set()) {
30
+ options?.signal?.throwIfAborted();
31
+ let currentUrl = new URL(url).href;
32
+ await validateUrl(currentUrl);
33
+ visited.add(currentUrl);
34
+ const originalRequest = createActivityPubRequest(currentUrl, { userAgent });
35
+ function follow(nextUrl) {
36
+ options?.signal?.throwIfAborted();
37
+ if (redirected >= DEFAULT_MAX_REDIRECTION) throw new FetchError(currentUrl, `Too many redirections (${redirected + 1})`);
38
+ if (visited.has(nextUrl)) throw new FetchError(currentUrl, `Redirect loop detected: ${nextUrl}`);
39
+ redirected++;
40
+ }
41
+ const response = await doubleKnock(originalRequest, identity, {
29
42
  specDeterminer,
30
43
  log: curry(logRequest)(logger$1),
31
44
  tracerProvider,
32
45
  signal: options?.signal,
33
- validateRedirect: validateUrl
34
- }), load);
46
+ validateRedirect: async (nextUrl) => {
47
+ follow(nextUrl);
48
+ await validateUrl(nextUrl);
49
+ visited.add(nextUrl);
50
+ currentUrl = nextUrl;
51
+ }
52
+ });
53
+ return getRemoteDocument(currentUrl, response, (alternateUrl) => {
54
+ follow(alternateUrl);
55
+ return load(alternateUrl, options, redirected, visited);
56
+ });
35
57
  }
36
58
  async function validateUrl(url) {
37
59
  if (!allowPrivateAddress) try {
38
60
  await validatePublicUrl(url);
39
61
  } catch (error) {
40
- if (error instanceof UrlError) logger$1.error("Disallowed private URL: {url}", {
62
+ if (error instanceof UrlError) if (error.reason === "dns") logger$1.debug("DNS lookup failed for {url}", {
63
+ url,
64
+ error
65
+ });
66
+ else logger$1.error("Disallowed private URL: {url}", {
41
67
  url,
42
68
  error
43
69
  });
44
70
  throw error;
45
71
  }
46
72
  }
47
- return load;
73
+ return (url, options) => load(url, options);
48
74
  }
49
75
  //#endregion
50
76
  //#region src/utils/kv-cache.ts
@@ -1,8 +1,8 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-B_Gwkjh7.mjs";
5
- import { n as fetchKey, o as validateCryptoKey } from "./key-DywPPmb0.mjs";
4
+ import { n as version, t as name } from "./deno-9AaebKOA.mjs";
5
+ import { r as fetchKey, s as validateCryptoKey } from "./key-x9f-H7eO.mjs";
6
6
  import { Activity, CryptographicKey, Object as Object$1, getTypeId } from "@fedify/vocab";
7
7
  import { SpanStatusCode, trace } from "@opentelemetry/api";
8
8
  import { getDocumentLoader } from "@fedify/vocab-runtime";
@@ -203,7 +203,7 @@ function normalizeContextReference(reference, baseUrl) {
203
203
  /** @internal */
204
204
  function isInvalidUrlTypeError(error) {
205
205
  const code = error.code;
206
- return error instanceof TypeError && (code === "ERR_INVALID_URL" || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
206
+ return error instanceof TypeError && (code === "ERR_INVALID_URL" || error.message === "Invalid URL string." || /^Invalid URL(?::|$)/.test(error.message) || / cannot be parsed as a URL\.?$/.test(error.message));
207
207
  }
208
208
  async function applyGraphAliasContext(state, context, documentLoader, remoteContextCache, baseUrl = null, processingContexts = /* @__PURE__ */ new Set()) {
209
209
  if (context === null) return createGraphAliasContextState();
@@ -1,4 +1,4 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
- const require_middleware = require("./middleware-CUNIlYzF.cjs");
3
+ const require_middleware = require("./middleware-XXuSt_6b.cjs");
4
4
  exports.FederationImpl = require_middleware.FederationImpl;
@@ -2,10 +2,10 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./chunk-CRNNMoPX.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-BGMIq1cs.js";
5
- import { _ as version, a as verifyRequestDetailed, d as validateCryptoKey, f as formatAcceptSignature, g as name, i as verifyRequest, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, u as importJwk } from "./http-88deUx3Y.js";
6
- import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-K3inAqnp.js";
7
- import { n as getNodeInfo, t as nodeInfoToJson } from "./types-DTONwNqL.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-rHQOc5rE.js";
5
+ import { a as verifyRequestDetailed, d as importJwk, f as validateCryptoKey, i as verifyRequest, m as formatAcceptSignature, n as parseRfc9421SignatureInput, o as exportJwk, t as doubleKnock, v as name, y as version } from "./http-CdJ6n4rA.js";
6
+ import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-miAYfiGO.js";
7
+ import { _ as signJsonLd, a as doesActorOwnKey, c as assertSafeJsonLd, f as detachSignature, g as isInvalidUrlTypeError, h as isClearlyMalformedContextReference, m as hasSignature, n as signObject, o as getKeyOwner, p as getNormalizationContextLoader, r as verifyObject, s as InvalidContextReferenceError, u as compactJsonLd, v as verifyCompactJsonLd, x as wrapContextLoaderForJsonLd } from "./proof-Dsa_ZC-M.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-Dx3QwNBI.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
11
11
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -13,7 +13,7 @@ import { cloneDeep, uniq } from "es-toolkit";
13
13
  import { Router } from "uri-template-router";
14
14
  import { parseTemplate } from "url-template";
15
15
  import { encodeHex } from "byte-encodings/hex";
16
- import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
16
+ import { FetchError, UrlError, getDocumentLoader, validatePublicUrl } from "@fedify/vocab-runtime";
17
17
  import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
18
18
  import jsonld from "@fedify/vocab-runtime/jsonld";
19
19
  import { lookupWebFinger } from "@fedify/webfinger";
@@ -861,6 +861,7 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
861
861
  }
862
862
  //#endregion
863
863
  //#region src/federation/keycache.ts
864
+ const KEY_CACHE_GENERATION = "2";
864
865
  var KvKeyCache = class {
865
866
  kv;
866
867
  prefix;
@@ -881,13 +882,20 @@ var KvKeyCache = class {
881
882
  keyId.href
882
883
  ];
883
884
  }
885
+ #entryKey(keyId) {
886
+ return [
887
+ ...this.prefix,
888
+ KEY_CACHE_GENERATION,
889
+ keyId.href
890
+ ];
891
+ }
884
892
  async get(keyId) {
885
893
  const negativeExpiration = this.nullKeys.get(keyId.href);
886
894
  if (negativeExpiration != null) {
887
895
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
888
896
  this.nullKeys.delete(keyId.href);
889
897
  }
890
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
898
+ const serialized = await this.kv.get(this.#entryKey(keyId));
891
899
  if (serialized === void 0) return void 0;
892
900
  if (serialized === null) {
893
901
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -899,7 +907,7 @@ var KvKeyCache = class {
899
907
  try {
900
908
  return await Multikey.fromJsonLd(serialized, this.options);
901
909
  } catch {
902
- await this.kv.delete([...this.prefix, keyId.href]);
910
+ await this.kv.delete(this.#entryKey(keyId));
903
911
  return;
904
912
  }
905
913
  }
@@ -907,12 +915,12 @@ var KvKeyCache = class {
907
915
  async set(keyId, key) {
908
916
  if (key == null) {
909
917
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
910
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
918
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
911
919
  return;
912
920
  }
913
921
  this.nullKeys.delete(keyId.href);
914
922
  const serialized = await key.toJsonLd(this.options);
915
- await this.kv.set([...this.prefix, keyId.href], serialized);
923
+ await this.kv.set(this.#entryKey(keyId), serialized);
916
924
  }
917
925
  async getFetchError(keyId) {
918
926
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1426,8 +1434,16 @@ async function handleInboxInternal(request, parameters, span) {
1426
1434
  }
1427
1435
  let json;
1428
1436
  try {
1429
- json = await request.clone().json();
1437
+ json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
1430
1438
  } catch (error) {
1439
+ if (error instanceof BodyTooLargeError) {
1440
+ request.body?.cancel(error).catch(() => {});
1441
+ span.setStatus({
1442
+ code: SpanStatusCode.ERROR,
1443
+ message: String(error)
1444
+ });
1445
+ return new Response("Inbox body too large.", { status: 413 });
1446
+ }
1431
1447
  logger.error("Failed to parse JSON:\n{error}", {
1432
1448
  recipient,
1433
1449
  error
@@ -2444,18 +2460,40 @@ async function readLimitedResponseBody(response, maxBytes) {
2444
2460
  if (truncated) result += "… (truncated)";
2445
2461
  return result;
2446
2462
  }
2447
- async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
2463
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2448
2464
  const logger = getLogger([
2449
2465
  "fedify",
2450
2466
  "federation",
2451
2467
  "outbox"
2452
2468
  ]);
2469
+ async function validateUrl(url) {
2470
+ if (!allowPrivateAddress) try {
2471
+ await validatePublicUrl(url);
2472
+ } catch (error) {
2473
+ if (error instanceof UrlError && error.reason === "dns") {
2474
+ logger.error("DNS resolution failed for URL: {url}", {
2475
+ url,
2476
+ error
2477
+ });
2478
+ const failure = new FetchError(url, error.message);
2479
+ failure.cause = error;
2480
+ throw failure;
2481
+ }
2482
+ if (error instanceof UrlError) logger.error("Disallowed private URL: {url}", {
2483
+ url,
2484
+ error
2485
+ });
2486
+ throw error;
2487
+ }
2488
+ }
2489
+ await validateUrl(inbox.href);
2453
2490
  headers = new Headers(headers);
2454
2491
  headers.set("Content-Type", "application/activity+json");
2455
2492
  const request = new Request(inbox, {
2456
2493
  method: "POST",
2457
2494
  headers,
2458
- body: JSON.stringify(activity)
2495
+ body: JSON.stringify(activity),
2496
+ redirect: "manual"
2459
2497
  });
2460
2498
  let rsaKey = null;
2461
2499
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -2471,9 +2509,10 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2471
2509
  });
2472
2510
  let response;
2473
2511
  try {
2474
- response = rsaKey == null ? await fetch(request) : await doubleKnock(request, rsaKey, {
2512
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await doubleKnock(request, rsaKey, {
2475
2513
  tracerProvider,
2476
- specDeterminer
2514
+ specDeterminer,
2515
+ validateRedirect: validateUrl
2477
2516
  });
2478
2517
  } catch (error) {
2479
2518
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
@@ -2505,6 +2544,49 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2505
2544
  "activitypub.activity.id": activityId ?? ""
2506
2545
  });
2507
2546
  }
2547
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2548
+ let body = await request.clone().text();
2549
+ for (let redirects = 0;; redirects++) {
2550
+ const response = await fetch(request, { redirect: "manual" });
2551
+ const location = response.headers.get("Location");
2552
+ if (![
2553
+ 301,
2554
+ 302,
2555
+ 303,
2556
+ 307,
2557
+ 308
2558
+ ].includes(response.status) || location == null) return response;
2559
+ await response.body?.cancel();
2560
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2561
+ const url = new URL(location, request.url);
2562
+ await validateUrl(url.href);
2563
+ const headers = new Headers(request.headers);
2564
+ if (url.origin !== new URL(request.url).origin) {
2565
+ headers.delete("Authorization");
2566
+ headers.delete("Proxy-Authorization");
2567
+ headers.delete("Cookie");
2568
+ headers.delete("Host");
2569
+ }
2570
+ let method = request.method;
2571
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2572
+ method = "GET";
2573
+ body = void 0;
2574
+ for (const name of [
2575
+ "Content-Encoding",
2576
+ "Content-Language",
2577
+ "Content-Length",
2578
+ "Content-Location",
2579
+ "Content-Type"
2580
+ ]) headers.delete(name);
2581
+ }
2582
+ request = new Request(url, {
2583
+ method,
2584
+ headers,
2585
+ body,
2586
+ redirect: "manual"
2587
+ });
2588
+ }
2589
+ }
2508
2590
  /**
2509
2591
  * An error that is thrown when an activity fails to send to a remote inbox.
2510
2592
  * It contains structured information about the failure, including the HTTP
@@ -3013,6 +3095,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3013
3095
  }
3014
3096
  try {
3015
3097
  await sendActivity({
3098
+ allowPrivateAddress: this.allowPrivateAddress,
3016
3099
  keys,
3017
3100
  activity: message.activity,
3018
3101
  activityId: message.activityId,
@@ -3404,6 +3487,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3404
3487
  });
3405
3488
  const promises = [];
3406
3489
  for (const inbox in inboxes) promises.push(sendActivity({
3490
+ allowPrivateAddress: this.allowPrivateAddress,
3407
3491
  keys,
3408
3492
  activity: jsonLd,
3409
3493
  activityId: activity.id?.href,
@@ -4562,6 +4646,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4562
4646
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4563
4647
  const promises = [];
4564
4648
  for (const inbox in inboxes) promises.push(sendActivity({
4649
+ allowPrivateAddress: this.federation.allowPrivateAddress,
4565
4650
  keys,
4566
4651
  activity: this.activity,
4567
4652
  activityId: this.activityId,
@@ -1,5 +1,5 @@
1
1
  import "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as FederationImpl } from "./middleware-D2O8b03q.mjs";
4
+ import { n as FederationImpl } from "./middleware-DHHRwSZH.mjs";
5
5
  export { FederationImpl };
@@ -2,25 +2,25 @@ import { Temporal } from "@js-temporal/polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { n as RouterError } from "./router-CrMLXoOr.mjs";
5
- import { n as version, t as name } from "./deno-B_Gwkjh7.mjs";
6
- import { t as formatAcceptSignature } from "./accept-Dd__NiUL.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-DywPPmb0.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-DIh9_s_-.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-B-IG1dVk.mjs";
10
- import { n as kvCache } from "./kv-cache-FsT1ljhs.mjs";
11
- import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-Ch6FVq7X.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Cygg-fHW.mjs";
13
- import { n as signObject, r as verifyObject } from "./proof-Dt5GV-xi.mjs";
14
- import { t as getNodeInfo } from "./client-CZWLyLmp.mjs";
15
- import { t as nodeInfoToJson } from "./types-DCP0WLdt.mjs";
16
- import { n as routeActivity } from "./inbox-BxCHIWPD.mjs";
17
- import { t as FederationBuilderImpl } from "./builder-BlCsjkrX.mjs";
18
- import { t as buildCollectionSynchronizationHeader } from "./collection-BD6-SZ6O.mjs";
19
- import { t as KvKeyCache } from "./keycache-CCSwkQcY.mjs";
20
- import { t as acceptsJsonLd } from "./negotiation-DnsfFF8I.mjs";
21
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-G_GDghC-.mjs";
22
- import { t as createExponentialBackoffPolicy } from "./retry-CJL0poaU.mjs";
23
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CqaJDT2z.mjs";
5
+ import { n as version, t as name } from "./deno-9AaebKOA.mjs";
6
+ import { t as formatAcceptSignature } from "./accept-BavzedPB.mjs";
7
+ import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-x9f-H7eO.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-B5qAeoJd.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-DX96uPUj.mjs";
10
+ import { n as kvCache } from "./kv-cache-DqEKe5E0.mjs";
11
+ import { a as compactJsonLd, c as getNormalizationContextLoader, d as isInvalidUrlTypeError, f as signJsonLd, g as wrapContextLoaderForJsonLd, l as hasSignature, p as verifyCompactJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as isClearlyMalformedContextReference } from "./ld-cfUwoi0C.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DklgvZXn.mjs";
13
+ import { n as signObject, r as verifyObject } from "./proof-CI4rsVLu.mjs";
14
+ import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DM72NZuk.mjs";
15
+ import { t as nodeInfoToJson } from "./types-C37hquWI.mjs";
16
+ import { n as routeActivity } from "./inbox-BSyjaFbo.mjs";
17
+ import { t as FederationBuilderImpl } from "./builder-DsBAef6b.mjs";
18
+ import { t as buildCollectionSynchronizationHeader } from "./collection-ChgDTHLz.mjs";
19
+ import { t as KvKeyCache } from "./keycache-CaO7wyQA.mjs";
20
+ import { t as acceptsJsonLd } from "./negotiation-BehA2uul.mjs";
21
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BanFifxu.mjs";
22
+ import { t as createExponentialBackoffPolicy } from "./retry-B_E3V_Dx.mjs";
23
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-DJmW6Z-I.mjs";
24
24
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
25
25
  import { lookupWebFinger } from "@fedify/webfinger";
26
26
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -476,8 +476,16 @@ async function handleInboxInternal(request, parameters, span) {
476
476
  }
477
477
  let json;
478
478
  try {
479
- json = await request.clone().json();
479
+ json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
480
480
  } catch (error) {
481
+ if (error instanceof BodyTooLargeError) {
482
+ request.body?.cancel(error).catch(() => {});
483
+ span.setStatus({
484
+ code: SpanStatusCode.ERROR,
485
+ message: String(error)
486
+ });
487
+ return new Response("Inbox body too large.", { status: 413 });
488
+ }
481
489
  logger.error("Failed to parse JSON:\n{error}", {
482
490
  recipient,
483
491
  error
@@ -1806,6 +1814,7 @@ var FederationImpl = class extends FederationBuilderImpl {
1806
1814
  }
1807
1815
  try {
1808
1816
  await sendActivity({
1817
+ allowPrivateAddress: this.allowPrivateAddress,
1809
1818
  keys,
1810
1819
  activity: message.activity,
1811
1820
  activityId: message.activityId,
@@ -2197,6 +2206,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2197
2206
  });
2198
2207
  const promises = [];
2199
2208
  for (const inbox in inboxes) promises.push(sendActivity({
2209
+ allowPrivateAddress: this.allowPrivateAddress,
2200
2210
  keys,
2201
2211
  activity: jsonLd,
2202
2212
  activityId: activity.id?.href,
@@ -3355,6 +3365,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
3355
3365
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
3356
3366
  const promises = [];
3357
3367
  for (const inbox in inboxes) promises.push(sendActivity({
3368
+ allowPrivateAddress: this.federation.allowPrivateAddress,
3358
3369
  keys,
3359
3370
  activity: this.activity,
3360
3371
  activityId: this.activityId,
@@ -2,10 +2,10 @@ const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  const require_chunk = require("./chunk-DDcVe30Y.cjs");
4
4
  const require_transformers = require("./transformers-NeAONrAq.cjs");
5
- const require_http = require("./http-Yoq0YGz7.cjs");
6
- const require_proof = require("./proof-CEn2R6W9.cjs");
7
- const require_types = require("./types-m8u9L86X.cjs");
8
- const require_kv_cache = require("./kv-cache-HA011rBG.cjs");
5
+ const require_http = require("./http-BJEMpnnY.cjs");
6
+ const require_types = require("./types-DRPjmHVV.cjs");
7
+ const require_proof = require("./proof-DzFzyfIc.cjs");
8
+ const require_kv_cache = require("./kv-cache-BDp2c_QE.cjs");
9
9
  let _logtape_logtape = require("@logtape/logtape");
10
10
  let _fedify_vocab = require("@fedify/vocab");
11
11
  let _opentelemetry_api = require("@opentelemetry/api");
@@ -336,7 +336,7 @@ var FederationBuilderImpl = class {
336
336
  this.collectionTypeIds = {};
337
337
  }
338
338
  async build(options) {
339
- const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-itcr4fAv.cjs"));
339
+ const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-BXJpAXhO.cjs"));
340
340
  const f = new FederationImpl(options);
341
341
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
342
342
  f.router = this.router.clone();
@@ -862,6 +862,7 @@ async function buildCollectionSynchronizationHeader(collectionId, actorIds) {
862
862
  }
863
863
  //#endregion
864
864
  //#region src/federation/keycache.ts
865
+ const KEY_CACHE_GENERATION = "2";
865
866
  var KvKeyCache = class {
866
867
  kv;
867
868
  prefix;
@@ -882,13 +883,20 @@ var KvKeyCache = class {
882
883
  keyId.href
883
884
  ];
884
885
  }
886
+ #entryKey(keyId) {
887
+ return [
888
+ ...this.prefix,
889
+ KEY_CACHE_GENERATION,
890
+ keyId.href
891
+ ];
892
+ }
885
893
  async get(keyId) {
886
894
  const negativeExpiration = this.nullKeys.get(keyId.href);
887
895
  if (negativeExpiration != null) {
888
896
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
889
897
  this.nullKeys.delete(keyId.href);
890
898
  }
891
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
899
+ const serialized = await this.kv.get(this.#entryKey(keyId));
892
900
  if (serialized === void 0) return void 0;
893
901
  if (serialized === null) {
894
902
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -900,7 +908,7 @@ var KvKeyCache = class {
900
908
  try {
901
909
  return await _fedify_vocab.Multikey.fromJsonLd(serialized, this.options);
902
910
  } catch {
903
- await this.kv.delete([...this.prefix, keyId.href]);
911
+ await this.kv.delete(this.#entryKey(keyId));
904
912
  return;
905
913
  }
906
914
  }
@@ -908,12 +916,12 @@ var KvKeyCache = class {
908
916
  async set(keyId, key) {
909
917
  if (key == null) {
910
918
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
911
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
919
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
912
920
  return;
913
921
  }
914
922
  this.nullKeys.delete(keyId.href);
915
923
  const serialized = await key.toJsonLd(this.options);
916
- await this.kv.set([...this.prefix, keyId.href], serialized);
924
+ await this.kv.set(this.#entryKey(keyId), serialized);
917
925
  }
918
926
  async getFetchError(keyId) {
919
927
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1427,8 +1435,16 @@ async function handleInboxInternal(request, parameters, span) {
1427
1435
  }
1428
1436
  let json;
1429
1437
  try {
1430
- json = await request.clone().json();
1438
+ json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
1431
1439
  } catch (error) {
1440
+ if (error instanceof require_types.BodyTooLargeError) {
1441
+ request.body?.cancel(error).catch(() => {});
1442
+ span.setStatus({
1443
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
1444
+ message: String(error)
1445
+ });
1446
+ return new Response("Inbox body too large.", { status: 413 });
1447
+ }
1432
1448
  logger.error("Failed to parse JSON:\n{error}", {
1433
1449
  recipient,
1434
1450
  error
@@ -2445,18 +2461,40 @@ async function readLimitedResponseBody(response, maxBytes) {
2445
2461
  if (truncated) result += "… (truncated)";
2446
2462
  return result;
2447
2463
  }
2448
- async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
2464
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2449
2465
  const logger = (0, _logtape_logtape.getLogger)([
2450
2466
  "fedify",
2451
2467
  "federation",
2452
2468
  "outbox"
2453
2469
  ]);
2470
+ async function validateUrl(url) {
2471
+ if (!allowPrivateAddress) try {
2472
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2473
+ } catch (error) {
2474
+ if (error instanceof _fedify_vocab_runtime.UrlError && error.reason === "dns") {
2475
+ logger.error("DNS resolution failed for URL: {url}", {
2476
+ url,
2477
+ error
2478
+ });
2479
+ const failure = new _fedify_vocab_runtime.FetchError(url, error.message);
2480
+ failure.cause = error;
2481
+ throw failure;
2482
+ }
2483
+ if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
2484
+ url,
2485
+ error
2486
+ });
2487
+ throw error;
2488
+ }
2489
+ }
2490
+ await validateUrl(inbox.href);
2454
2491
  headers = new Headers(headers);
2455
2492
  headers.set("Content-Type", "application/activity+json");
2456
2493
  const request = new Request(inbox, {
2457
2494
  method: "POST",
2458
2495
  headers,
2459
- body: JSON.stringify(activity)
2496
+ body: JSON.stringify(activity),
2497
+ redirect: "manual"
2460
2498
  });
2461
2499
  let rsaKey = null;
2462
2500
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -2472,9 +2510,10 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2472
2510
  });
2473
2511
  let response;
2474
2512
  try {
2475
- response = rsaKey == null ? await fetch(request) : await require_http.doubleKnock(request, rsaKey, {
2513
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2476
2514
  tracerProvider,
2477
- specDeterminer
2515
+ specDeterminer,
2516
+ validateRedirect: validateUrl
2478
2517
  });
2479
2518
  } catch (error) {
2480
2519
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
@@ -2506,6 +2545,49 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2506
2545
  "activitypub.activity.id": activityId ?? ""
2507
2546
  });
2508
2547
  }
2548
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2549
+ let body = await request.clone().text();
2550
+ for (let redirects = 0;; redirects++) {
2551
+ const response = await fetch(request, { redirect: "manual" });
2552
+ const location = response.headers.get("Location");
2553
+ if (![
2554
+ 301,
2555
+ 302,
2556
+ 303,
2557
+ 307,
2558
+ 308
2559
+ ].includes(response.status) || location == null) return response;
2560
+ await response.body?.cancel();
2561
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2562
+ const url = new URL(location, request.url);
2563
+ await validateUrl(url.href);
2564
+ const headers = new Headers(request.headers);
2565
+ if (url.origin !== new URL(request.url).origin) {
2566
+ headers.delete("Authorization");
2567
+ headers.delete("Proxy-Authorization");
2568
+ headers.delete("Cookie");
2569
+ headers.delete("Host");
2570
+ }
2571
+ let method = request.method;
2572
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2573
+ method = "GET";
2574
+ body = void 0;
2575
+ for (const name of [
2576
+ "Content-Encoding",
2577
+ "Content-Language",
2578
+ "Content-Length",
2579
+ "Content-Location",
2580
+ "Content-Type"
2581
+ ]) headers.delete(name);
2582
+ }
2583
+ request = new Request(url, {
2584
+ method,
2585
+ headers,
2586
+ body,
2587
+ redirect: "manual"
2588
+ });
2589
+ }
2590
+ }
2509
2591
  /**
2510
2592
  * An error that is thrown when an activity fails to send to a remote inbox.
2511
2593
  * It contains structured information about the failure, including the HTTP
@@ -3007,6 +3089,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3007
3089
  }
3008
3090
  try {
3009
3091
  await sendActivity({
3092
+ allowPrivateAddress: this.allowPrivateAddress,
3010
3093
  keys,
3011
3094
  activity: message.activity,
3012
3095
  activityId: message.activityId,
@@ -3398,6 +3481,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3398
3481
  });
3399
3482
  const promises = [];
3400
3483
  for (const inbox in inboxes) promises.push(sendActivity({
3484
+ allowPrivateAddress: this.allowPrivateAddress,
3401
3485
  keys,
3402
3486
  activity: jsonLd,
3403
3487
  activityId: activity.id?.href,
@@ -4556,6 +4640,7 @@ var InboxContextImpl = class InboxContextImpl extends ContextImpl {
4556
4640
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4557
4641
  const promises = [];
4558
4642
  for (const inbox in inboxes) promises.push(sendActivity({
4643
+ allowPrivateAddress: this.federation.allowPrivateAddress,
4559
4644
  keys,
4560
4645
  activity: this.activity,
4561
4646
  activityId: this.activityId,
@@ -1,5 +1,5 @@
1
1
  /// <reference lib="esnext.temporal" />
2
- import { S as KeyCache } from "./http-BDZeS5om.js";
2
+ import { S as KeyCache } from "./http-Dl00HXr4.js";
3
3
  import { CryptographicKey, DataIntegrityProof, Multikey, Object as Object$1 } from "@fedify/vocab";
4
4
  import { TracerProvider } from "@opentelemetry/api";
5
5
  import { DocumentLoader } from "@fedify/vocab-runtime";