@fedify/fedify 2.2.12 → 2.2.14-dev.2082

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 (102) hide show
  1. package/dist/{assert_instance_of-C4Ri6VuN.mjs → assert-BhQd8QHP.mjs} +19 -1
  2. package/dist/{builder-TJzcpmDm.mjs → builder-Cwgo1nK5.mjs} +3 -3
  3. package/dist/{client-dx126nMS.mjs → client-CQ7zd8tW.mjs} +78 -4
  4. package/dist/compat/mod.d.cts +1 -1
  5. package/dist/compat/mod.d.ts +1 -1
  6. package/dist/compat/outgoing-jsonld.test.mjs +1 -1
  7. package/dist/compat/public-audience.test.mjs +1 -1
  8. package/dist/compat/transformers.test.mjs +3 -4
  9. package/dist/{context-BU-1O90h.d.ts → context-Bt2jnbC7.d.ts} +4 -3
  10. package/dist/{context-DVA8wHZ0.d.cts → context-Cls_M1HU.d.cts} +4 -3
  11. package/dist/{deno-DeYUdFal.mjs → deno-CV4wsQzy.mjs} +1 -1
  12. package/dist/{docloader-CXH2XqYP.mjs → docloader-DV9F08Dn.mjs} +2 -2
  13. package/dist/federation/builder.test.mjs +2 -2
  14. package/dist/federation/collection.test.mjs +1 -1
  15. package/dist/federation/handler.test.mjs +127 -10
  16. package/dist/federation/idempotency.test.mjs +4 -4
  17. package/dist/federation/inbox.test.mjs +1 -1
  18. package/dist/federation/keycache.test.mjs +44 -10
  19. package/dist/federation/kv.test.mjs +1 -1
  20. package/dist/federation/middleware.test.mjs +103 -15
  21. package/dist/federation/mod.cjs +1 -1
  22. package/dist/federation/mod.d.cts +2 -2
  23. package/dist/federation/mod.d.ts +2 -2
  24. package/dist/federation/mod.js +1 -1
  25. package/dist/federation/mq.test.mjs +1 -1
  26. package/dist/federation/negotiation.test.mjs +2 -2
  27. package/dist/federation/retry.test.mjs +1 -1
  28. package/dist/federation/router.test.mjs +1 -1
  29. package/dist/federation/send.test.mjs +138 -6
  30. package/dist/federation/temporal.test.mjs +2 -2
  31. package/dist/federation/webfinger.test.mjs +2 -2
  32. package/dist/{http-D6cIKrI1.cjs → http-BWNuy1sb.cjs} +166 -6
  33. package/dist/{http-CNsxTbVr.js → http-C5T-lWqB.js} +155 -7
  34. package/dist/{http-79lZeYEo.mjs → http-C9MzFrOj.mjs} +3 -3
  35. package/dist/{http-BDZeS5om.d.ts → http-Dl00HXr4.d.ts} +1 -1
  36. package/dist/{http-C87EWkO0.d.cts → http-DqZhyEso.d.cts} +1 -1
  37. package/dist/{key-B4kUuy3o.mjs → key-DiziIx9e.mjs} +154 -6
  38. package/dist/{keycache-EGATflN-.mjs → keycache-C1aVprwy.mjs} +12 -4
  39. package/dist/{kv-cache-CIZ1m-94.js → kv-cache-CM-zJoep.js} +1 -1
  40. package/dist/{kv-cache-BtXEUvHT.cjs → kv-cache-DGPEHc1T.cjs} +1 -1
  41. package/dist/{ld-2R9vZ_ym.mjs → ld-BAeuDKjx.mjs} +2 -2
  42. package/dist/{middleware-CjrIRAsQ.cjs → middleware-BbbKc-Tr.cjs} +90 -14
  43. package/dist/{middleware-D5NE6PkU.mjs → middleware-CkJOjZjS.mjs} +31 -20
  44. package/dist/{middleware-_qt3kHUU.mjs → middleware-DA2CvTQL.mjs} +1 -1
  45. package/dist/{middleware-DsEAPFhi.js → middleware-J32whqNP.js} +90 -14
  46. package/dist/{middleware-2bSnAJOQ.cjs → middleware-XNvq3NUZ.cjs} +1 -1
  47. package/dist/{mod-BhU_H1I_.d.ts → mod-0MWo2Nu5.d.ts} +1 -1
  48. package/dist/{mod-DXY9JF28.d.cts → mod-D2JyWtW3.d.cts} +1 -1
  49. package/dist/{mod-q-NFLW6B.d.cts → mod-DNvnDTS7.d.cts} +1 -1
  50. package/dist/{mod-BVt6iTmH.d.ts → mod-DaiJDJdQ.d.ts} +1 -1
  51. package/dist/{mod-B0rWmfW5.d.cts → mod-DzPMtIV3.d.cts} +1 -1
  52. package/dist/{mod-DHO9lk3D.d.ts → mod-ej_rLx8h.d.ts} +1 -1
  53. package/dist/mod.cjs +5 -5
  54. package/dist/mod.d.cts +6 -6
  55. package/dist/mod.d.ts +6 -6
  56. package/dist/mod.js +5 -5
  57. package/dist/nodeinfo/client.test.mjs +28 -1
  58. package/dist/nodeinfo/handler.test.mjs +2 -2
  59. package/dist/nodeinfo/mod.cjs +1 -1
  60. package/dist/nodeinfo/mod.js +1 -1
  61. package/dist/nodeinfo/types.test.mjs +1 -1
  62. package/dist/otel/exporter.test.mjs +1 -1
  63. package/dist/{outgoing-jsonld-CfUItkGW.mjs → outgoing-jsonld-Dc2OXyB-.mjs} +1 -1
  64. package/dist/{owner-CnngXDNJ.d.ts → owner-B3hNQCG8.d.cts} +12 -1
  65. package/dist/{owner-DEvZuyOE.d.cts → owner-CiBPcC6F.d.ts} +12 -1
  66. package/dist/owner-D08_JUSb.mjs +175 -0
  67. package/dist/{proof-CmtXtaul.cjs → proof-7T47tIu5.cjs} +112 -58
  68. package/dist/{proof-BhW_iARR.js → proof-BdRnLnh7.js} +112 -58
  69. package/dist/{proof-BzTt6nlk.mjs → proof-_S-wFUFM.mjs} +4 -4
  70. package/dist/{send-CZwOs7XD.mjs → send-DmcKKipr.mjs} +64 -6
  71. package/dist/sig/accept.test.mjs +1 -1
  72. package/dist/sig/http.test.mjs +4 -4
  73. package/dist/sig/key.test.mjs +170 -3
  74. package/dist/sig/ld.test.mjs +56 -4
  75. package/dist/sig/mod.cjs +2 -2
  76. package/dist/sig/mod.d.cts +3 -3
  77. package/dist/sig/mod.d.ts +3 -3
  78. package/dist/sig/mod.js +2 -2
  79. package/dist/sig/owner.test.mjs +105 -6
  80. package/dist/sig/proof.test.mjs +73 -5
  81. package/dist/{temporal-BYCnXIGk.mjs → temporal-CX_L28b-.mjs} +1 -1
  82. package/dist/{types-m8u9L86X.cjs → types-DWs-qZaY.cjs} +94 -2
  83. package/dist/{types-DTONwNqL.js → types-sXITXcWa.js} +78 -4
  84. package/dist/utils/docloader.test.mjs +28 -4
  85. package/dist/utils/kv-cache.test.mjs +1 -1
  86. package/dist/utils/mod.cjs +1 -1
  87. package/dist/utils/mod.d.cts +1 -1
  88. package/dist/utils/mod.d.ts +1 -1
  89. package/dist/utils/mod.js +1 -1
  90. package/package.json +5 -5
  91. package/dist/assert-DikXweDx.mjs +0 -23
  92. package/dist/owner-CqYsVXcd.mjs +0 -120
  93. /package/dist/{accept-CPkZzmGN.mjs → accept-Dd__NiUL.mjs} +0 -0
  94. /package/dist/{activity-listener-ell7W1s9.mjs → activity-listener-IojJ9SVl.mjs} +0 -0
  95. /package/dist/{collection-D-HqUuA2.mjs → collection-DnoYNwBC.mjs} +0 -0
  96. /package/dist/{keys-DGu1NFwu.mjs → keys-BAK-tUlf.mjs} +0 -0
  97. /package/dist/{kv-rV3vodCc.mjs → kv-CX8qfaad.mjs} +0 -0
  98. /package/dist/{kv-cache-DYsF2MhP.mjs → kv-cache-FsT1ljhs.mjs} +0 -0
  99. /package/dist/{negotiation-SQvQgUqe.mjs → negotiation-BHtZN6Y1.mjs} +0 -0
  100. /package/dist/{public-audience-Ci-r-4Q-.mjs → public-audience-CS1udGkN.mjs} +0 -0
  101. /package/dist/{retry-v_sGLH1d.mjs → retry-bMXBL97A.mjs} +0 -0
  102. /package/dist/{types-J53Kw7so.mjs → types-s4mCTGkT.mjs} +0 -0
@@ -1,7 +1,7 @@
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-DeYUdFal.mjs";
4
+ import { n as version, t as name } from "./deno-CV4wsQzy.mjs";
5
5
  import { CryptographicKey, Object as Object$1, isActor } from "@fedify/vocab";
6
6
  import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
7
7
  import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
@@ -85,6 +85,118 @@ async function importJwk(jwk, type) {
85
85
  validateCryptoKey(key, type);
86
86
  return key;
87
87
  }
88
+ /**
89
+ * Fetches the document that the given actor URI dereferences to, and returns
90
+ * the actor only if the host that served the document is authoritative for
91
+ * the id the document claims.
92
+ *
93
+ * Only the origin that serves an actor id can speak for it. Without that
94
+ * rule any host could serve a document describing somebody else's actor—and
95
+ * listing its own keys as that actor's.
96
+ *
97
+ * @param actorId The URI of the actor to fetch.
98
+ * @param options Options for fetching the document.
99
+ * @returns The actor, or `null` if the document cannot be fetched, is not an
100
+ * actor, or belongs to another origin.
101
+ * @internal
102
+ */
103
+ async function fetchActorDocument(actorId, options = {}) {
104
+ const logger = getLogger([
105
+ "fedify",
106
+ "sig",
107
+ "key"
108
+ ]);
109
+ const documentLoader = options.documentLoader ?? getDocumentLoader();
110
+ const contextLoader = options.contextLoader ?? getDocumentLoader();
111
+ const { tracerProvider } = options;
112
+ let document;
113
+ let documentUrl = actorId;
114
+ try {
115
+ const remoteDocument = await documentLoader(actorId.href);
116
+ document = remoteDocument.document;
117
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", actorId);
118
+ } catch (error) {
119
+ logger.debug("Failed to fetch the actor {actorId}: {error}", {
120
+ actorId: actorId.href,
121
+ error
122
+ });
123
+ return null;
124
+ }
125
+ let object;
126
+ try {
127
+ object = await Object$1.fromJsonLd(document, {
128
+ documentLoader,
129
+ contextLoader,
130
+ tracerProvider,
131
+ baseUrl: documentUrl
132
+ });
133
+ } catch (error) {
134
+ if (!(error instanceof TypeError)) throw error;
135
+ logger.debug("The document served at {documentUrl} is not a valid object: {error}", {
136
+ documentUrl: documentUrl.href,
137
+ error
138
+ });
139
+ return null;
140
+ }
141
+ if (!isActor(object)) return null;
142
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
143
+ logger.debug("The document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin; refusing to treat it as that actor's document.", {
144
+ documentUrl: documentUrl.href,
145
+ actorId: object.id?.href
146
+ });
147
+ return null;
148
+ }
149
+ return object;
150
+ }
151
+ /**
152
+ * Resolves the owner that the given key claims, and returns it only if the
153
+ * claim holds.
154
+ *
155
+ * The `owner` of a {@link CryptographicKey}, like the `controller` of
156
+ * a {@link Multikey}, proves nothing on its own: the key document and the
157
+ * claim inside it are served by the same host, so anyone able to serve a key
158
+ * document can name any actor in the world as its owner. The claim becomes
159
+ * meaningful only once the named actor's own document is fetched and turns
160
+ * out to link back to the key. This function performs that mutual-link
161
+ * check, which is the only sound way to learn a fetched key's owner.
162
+ *
163
+ * The claimed owner is always dereferenced, never read out of the key
164
+ * document: an owner embedded there is written by whoever wrote the key.
165
+ *
166
+ * @param key The key whose ownership claim is to be verified. It must carry
167
+ * an `id`, as that is what the owner has to link back to.
168
+ * @param options Options for fetching the claimed owner.
169
+ * @returns The verified owner, or `null` if the key claims no owner, the
170
+ * owner cannot be fetched, or the owner does not link back to
171
+ * the key.
172
+ * @internal
173
+ */
174
+ async function verifyKeyOwnership(key, options = {}) {
175
+ const logger = getLogger([
176
+ "fedify",
177
+ "sig",
178
+ "key"
179
+ ]);
180
+ const keyId = key.id;
181
+ if (keyId == null) return null;
182
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
183
+ if (claimedOwnerId == null) return null;
184
+ const owner = await fetchActorDocument(claimedOwnerId, options);
185
+ if (owner == null) {
186
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims could not be resolved.", {
187
+ keyId: keyId.href,
188
+ claimedOwnerId: claimedOwnerId.href
189
+ });
190
+ return null;
191
+ }
192
+ const linkedKeyIds = key instanceof CryptographicKey ? owner.publicKeyIds : owner.assertionMethodIds;
193
+ for (const linkedKeyId of linkedKeyIds) if (linkedKeyId.href === keyId.href) return owner;
194
+ logger.debug("The owner ({claimedOwnerId}) that key {keyId} claims does not list the key as its own.", {
195
+ keyId: keyId.href,
196
+ claimedOwnerId: claimedOwnerId.href
197
+ });
198
+ return null;
199
+ }
88
200
  async function withFetchKeySpan(keyId, tracerProvider, fetcher) {
89
201
  tracerProvider ??= trace.getTracerProvider();
90
202
  return await tracerProvider.getTracer(name, version).startActiveSpan("activitypub.fetch_key", {
@@ -212,7 +324,7 @@ async function getCachedFetchKey(cacheKey, keyId, cls, keyCache, logger) {
212
324
  async function clearFetchErrorMetadata(keyId, keyCache) {
213
325
  await keyCache?.setFetchError?.(keyId, null);
214
326
  }
215
- async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
327
+ async function resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, { documentLoader, contextLoader, keyCache, tracerProvider }, logger) {
216
328
  let object;
217
329
  try {
218
330
  object = await Object$1.fromJsonLd(document, {
@@ -242,8 +354,22 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
242
354
  }
243
355
  }
244
356
  let key = null;
245
- if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object;
357
+ let ownerDocument = null;
358
+ if (object instanceof cls && (object.id == null || object.id.href === keyId)) key = object.id == null ? object.clone({ id: cacheKey }) : object;
246
359
  else if (isActor(object)) {
360
+ if (object.id == null || object.id.origin !== documentUrl.origin) {
361
+ logger.debug("Failed to verify; the document served at {documentUrl} claims to be the actor {actorId}, which belongs to another origin.", {
362
+ keyId,
363
+ documentUrl: documentUrl.href,
364
+ actorId: object.id?.href
365
+ });
366
+ await keyCache?.set(cacheKey, null);
367
+ return {
368
+ key: null,
369
+ cached: false
370
+ };
371
+ }
372
+ ownerDocument = object;
247
373
  const keys = cls === CryptographicKey ? object.getPublicKeys({
248
374
  documentLoader,
249
375
  contextLoader,
@@ -305,6 +431,25 @@ async function resolveFetchedKey(document, cacheKey, keyId, cls, { documentLoade
305
431
  cached: false
306
432
  };
307
433
  }
434
+ const claimedOwnerId = key instanceof CryptographicKey ? key.ownerId : key.controllerId;
435
+ if (ownerDocument != null && claimedOwnerId == null) key = key instanceof CryptographicKey ? key.clone({ owner: ownerDocument.id }) : key.clone({ controller: ownerDocument.id });
436
+ else if (claimedOwnerId != null && claimedOwnerId.href !== ownerDocument?.id?.href) {
437
+ if (await verifyKeyOwnership(key, {
438
+ documentLoader,
439
+ contextLoader,
440
+ tracerProvider
441
+ }) == null) {
442
+ logger.debug("Failed to verify; the owner {claimedOwnerId} that key {keyId} claims does not list the key as its own.", {
443
+ keyId,
444
+ claimedOwnerId: claimedOwnerId.href
445
+ });
446
+ await keyCache?.set(cacheKey, null);
447
+ return {
448
+ key: null,
449
+ cached: false
450
+ };
451
+ }
452
+ }
308
453
  if (keyCache != null) {
309
454
  await keyCache.set(cacheKey, key);
310
455
  logger.debug("Key {keyId} cached.", { keyId });
@@ -328,12 +473,15 @@ async function fetchKeyWithResult(cacheKey, cls, options, onCachedUnavailable, o
328
473
  if (cached != null) return cached;
329
474
  logger.debug("Fetching key {keyId} to verify signature...", { keyId });
330
475
  let document;
476
+ let documentUrl = cacheKey;
331
477
  try {
332
- document = (await (options.documentLoader ?? getDocumentLoader())(keyId)).document;
478
+ const remoteDocument = await (options.documentLoader ?? getDocumentLoader())(keyId);
479
+ document = remoteDocument.document;
480
+ documentUrl = new URL(remoteDocument.documentUrl ?? "", cacheKey);
333
481
  } catch (error) {
334
482
  return await onFetchError(error, cacheKey, keyId, keyCache, logger);
335
483
  }
336
- return await resolveFetchedKey(document, cacheKey, keyId, cls, options, logger);
484
+ return await resolveFetchedKey(document, cacheKey, documentUrl, keyId, cls, options, logger);
337
485
  }
338
486
  async function fetchKeyInternal(keyId, cls, options = {}) {
339
487
  return await fetchKeyWithResult(typeof keyId === "string" ? new URL(keyId) : keyId, cls, options, (_cacheKey, _keyId, _keyCache, _logger) => {
@@ -359,4 +507,4 @@ async function fetchKeyInternal(keyId, cls, options = {}) {
359
507
  });
360
508
  }
361
509
  //#endregion
362
- export { importJwk as a, generateCryptoKeyPair as i, fetchKey as n, validateCryptoKey as o, fetchKeyDetailed as r, exportJwk as t };
510
+ export { generateCryptoKeyPair as a, verifyKeyOwnership as c, fetchKeyDetailed as i, fetchActorDocument as n, importJwk as o, fetchKey as r, validateCryptoKey as s, exportJwk as t };
@@ -3,6 +3,7 @@ import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
4
  import { CryptographicKey, Multikey } from "@fedify/vocab";
5
5
  //#region src/federation/keycache.ts
6
+ const KEY_CACHE_GENERATION = "2";
6
7
  var KvKeyCache = class {
7
8
  kv;
8
9
  prefix;
@@ -23,13 +24,20 @@ var KvKeyCache = class {
23
24
  keyId.href
24
25
  ];
25
26
  }
27
+ #entryKey(keyId) {
28
+ return [
29
+ ...this.prefix,
30
+ KEY_CACHE_GENERATION,
31
+ keyId.href
32
+ ];
33
+ }
26
34
  async get(keyId) {
27
35
  const negativeExpiration = this.nullKeys.get(keyId.href);
28
36
  if (negativeExpiration != null) {
29
37
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
30
38
  this.nullKeys.delete(keyId.href);
31
39
  }
32
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
40
+ const serialized = await this.kv.get(this.#entryKey(keyId));
33
41
  if (serialized === void 0) return void 0;
34
42
  if (serialized === null) {
35
43
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -41,7 +49,7 @@ var KvKeyCache = class {
41
49
  try {
42
50
  return await Multikey.fromJsonLd(serialized, this.options);
43
51
  } catch {
44
- await this.kv.delete([...this.prefix, keyId.href]);
52
+ await this.kv.delete(this.#entryKey(keyId));
45
53
  return;
46
54
  }
47
55
  }
@@ -49,12 +57,12 @@ var KvKeyCache = class {
49
57
  async set(keyId, key) {
50
58
  if (key == null) {
51
59
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
52
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
60
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
53
61
  return;
54
62
  }
55
63
  this.nullKeys.delete(keyId.href);
56
64
  const serialized = await key.toJsonLd(this.options);
57
- await this.kv.set([...this.prefix, keyId.href], serialized);
65
+ await this.kv.set(this.#entryKey(keyId), serialized);
58
66
  }
59
67
  async getFetchError(keyId) {
60
68
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1,6 +1,6 @@
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-CNsxTbVr.js";
3
+ import { f as validateCryptoKey, t as doubleKnock } from "./http-C5T-lWqB.js";
4
4
  import { getLogger } from "@logtape/logtape";
5
5
  import { curry } from "es-toolkit";
6
6
  import { UrlError, createActivityPubRequest, getRemoteDocument, logRequest, preloadedContexts, validatePublicUrl } from "@fedify/vocab-runtime";
@@ -1,7 +1,7 @@
1
1
  const { Temporal } = require("@js-temporal/polyfill");
2
2
  const { URLPattern } = require("urlpattern-polyfill");
3
3
  require("./chunk-DDcVe30Y.cjs");
4
- const require_http = require("./http-D6cIKrI1.cjs");
4
+ const require_http = require("./http-BWNuy1sb.cjs");
5
5
  let _logtape_logtape = require("@logtape/logtape");
6
6
  let es_toolkit = require("es-toolkit");
7
7
  let _fedify_vocab_runtime = require("@fedify/vocab-runtime");
@@ -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-DeYUdFal.mjs";
5
- import { n as fetchKey, o as validateCryptoKey } from "./key-B4kUuy3o.mjs";
4
+ import { n as version, t as name } from "./deno-CV4wsQzy.mjs";
5
+ import { r as fetchKey, s as validateCryptoKey } from "./key-DiziIx9e.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";
@@ -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-D6cIKrI1.cjs");
6
- const require_proof = require("./proof-CmtXtaul.cjs");
7
- const require_types = require("./types-m8u9L86X.cjs");
8
- const require_kv_cache = require("./kv-cache-BtXEUvHT.cjs");
5
+ const require_http = require("./http-BWNuy1sb.cjs");
6
+ const require_types = require("./types-DWs-qZaY.cjs");
7
+ const require_proof = require("./proof-7T47tIu5.cjs");
8
+ const require_kv_cache = require("./kv-cache-DGPEHc1T.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");
@@ -212,7 +212,7 @@ var FederationBuilderImpl = class {
212
212
  this.collectionTypeIds = {};
213
213
  }
214
214
  async build(options) {
215
- const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-2bSnAJOQ.cjs"));
215
+ const { FederationImpl } = await Promise.resolve().then(() => require("./middleware-XNvq3NUZ.cjs"));
216
216
  const f = new FederationImpl(options);
217
217
  const trailingSlashInsensitiveValue = f.router.trailingSlashInsensitive;
218
218
  f.router = this.router.clone();
@@ -915,6 +915,7 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
915
915
  }
916
916
  //#endregion
917
917
  //#region src/federation/keycache.ts
918
+ const KEY_CACHE_GENERATION = "2";
918
919
  var KvKeyCache = class {
919
920
  kv;
920
921
  prefix;
@@ -935,13 +936,20 @@ var KvKeyCache = class {
935
936
  keyId.href
936
937
  ];
937
938
  }
939
+ #entryKey(keyId) {
940
+ return [
941
+ ...this.prefix,
942
+ KEY_CACHE_GENERATION,
943
+ keyId.href
944
+ ];
945
+ }
938
946
  async get(keyId) {
939
947
  const negativeExpiration = this.nullKeys.get(keyId.href);
940
948
  if (negativeExpiration != null) {
941
949
  if (Temporal.Now.instant().until(negativeExpiration).sign >= 0) return null;
942
950
  this.nullKeys.delete(keyId.href);
943
951
  }
944
- const serialized = await this.kv.get([...this.prefix, keyId.href]);
952
+ const serialized = await this.kv.get(this.#entryKey(keyId));
945
953
  if (serialized === void 0) return void 0;
946
954
  if (serialized === null) {
947
955
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
@@ -953,7 +961,7 @@ var KvKeyCache = class {
953
961
  try {
954
962
  return await _fedify_vocab.Multikey.fromJsonLd(serialized, this.options);
955
963
  } catch {
956
- await this.kv.delete([...this.prefix, keyId.href]);
964
+ await this.kv.delete(this.#entryKey(keyId));
957
965
  return;
958
966
  }
959
967
  }
@@ -961,12 +969,12 @@ var KvKeyCache = class {
961
969
  async set(keyId, key) {
962
970
  if (key == null) {
963
971
  this.nullKeys.set(keyId.href, Temporal.Now.instant().add(this.unavailableKeyTtl));
964
- await this.kv.set([...this.prefix, keyId.href], null, { ttl: this.unavailableKeyTtl });
972
+ await this.kv.set(this.#entryKey(keyId), null, { ttl: this.unavailableKeyTtl });
965
973
  return;
966
974
  }
967
975
  this.nullKeys.delete(keyId.href);
968
976
  const serialized = await key.toJsonLd(this.options);
969
- await this.kv.set([...this.prefix, keyId.href], serialized);
977
+ await this.kv.set(this.#entryKey(keyId), serialized);
970
978
  }
971
979
  async getFetchError(keyId) {
972
980
  const cached = await this.kv.get(this.#getFetchErrorKey(keyId));
@@ -1687,8 +1695,16 @@ async function handleInboxInternal(request, parameters, span) {
1687
1695
  }
1688
1696
  let json;
1689
1697
  try {
1690
- json = await request.clone().json();
1698
+ json = JSON.parse(await require_types.readBoundedText(request.clone(), require_types.MAX_BODY_SIZE, request.url));
1691
1699
  } catch (error) {
1700
+ if (error instanceof require_types.BodyTooLargeError) {
1701
+ request.body?.cancel(error).catch(() => {});
1702
+ span.setStatus({
1703
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
1704
+ message: String(error)
1705
+ });
1706
+ return new Response("Inbox body too large.", { status: 413 });
1707
+ }
1692
1708
  logger.error("Failed to parse JSON:\n{error}", {
1693
1709
  recipient,
1694
1710
  error
@@ -2705,18 +2721,31 @@ async function readLimitedResponseBody(response, maxBytes) {
2705
2721
  if (truncated) result += "… (truncated)";
2706
2722
  return result;
2707
2723
  }
2708
- async function sendActivityInternal({ activity, activityId, keys, inbox, headers, specDeterminer, tracerProvider }, span) {
2724
+ async function sendActivityInternal({ activity, activityId, keys, inbox, allowPrivateAddress, headers, specDeterminer, tracerProvider }, span) {
2709
2725
  const logger = (0, _logtape_logtape.getLogger)([
2710
2726
  "fedify",
2711
2727
  "federation",
2712
2728
  "outbox"
2713
2729
  ]);
2730
+ async function validateUrl(url) {
2731
+ if (!allowPrivateAddress) try {
2732
+ await (0, _fedify_vocab_runtime.validatePublicUrl)(url);
2733
+ } catch (error) {
2734
+ if (error instanceof _fedify_vocab_runtime.UrlError) logger.error("Disallowed private URL: {url}", {
2735
+ url,
2736
+ error
2737
+ });
2738
+ throw error;
2739
+ }
2740
+ }
2741
+ await validateUrl(inbox.href);
2714
2742
  headers = new Headers(headers);
2715
2743
  headers.set("Content-Type", "application/activity+json");
2716
2744
  const request = new Request(inbox, {
2717
2745
  method: "POST",
2718
2746
  headers,
2719
- body: JSON.stringify(activity)
2747
+ body: JSON.stringify(activity),
2748
+ redirect: "manual"
2720
2749
  });
2721
2750
  let rsaKey = null;
2722
2751
  for (const key of keys) if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") {
@@ -2732,9 +2761,10 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2732
2761
  });
2733
2762
  let response;
2734
2763
  try {
2735
- response = rsaKey == null ? await fetch(request) : await require_http.doubleKnock(request, rsaKey, {
2764
+ response = rsaKey == null ? await fetchWithValidatedRedirects(request, validateUrl) : await require_http.doubleKnock(request, rsaKey, {
2736
2765
  tracerProvider,
2737
- specDeterminer
2766
+ specDeterminer,
2767
+ validateRedirect: validateUrl
2738
2768
  });
2739
2769
  } catch (error) {
2740
2770
  logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", {
@@ -2766,6 +2796,49 @@ async function sendActivityInternal({ activity, activityId, keys, inbox, headers
2766
2796
  "activitypub.activity.id": activityId ?? ""
2767
2797
  });
2768
2798
  }
2799
+ async function fetchWithValidatedRedirects(request, validateUrl) {
2800
+ let body = await request.clone().text();
2801
+ for (let redirects = 0;; redirects++) {
2802
+ const response = await fetch(request, { redirect: "manual" });
2803
+ const location = response.headers.get("Location");
2804
+ if (![
2805
+ 301,
2806
+ 302,
2807
+ 303,
2808
+ 307,
2809
+ 308
2810
+ ].includes(response.status) || location == null) return response;
2811
+ await response.body?.cancel();
2812
+ if (redirects >= 20) throw new TypeError("Too many redirects");
2813
+ const url = new URL(location, request.url);
2814
+ await validateUrl(url.href);
2815
+ const headers = new Headers(request.headers);
2816
+ if (url.origin !== new URL(request.url).origin) {
2817
+ headers.delete("Authorization");
2818
+ headers.delete("Proxy-Authorization");
2819
+ headers.delete("Cookie");
2820
+ headers.delete("Host");
2821
+ }
2822
+ let method = request.method;
2823
+ if ((response.status === 301 || response.status === 302) && method === "POST" || response.status === 303 && method !== "GET" && method !== "HEAD") {
2824
+ method = "GET";
2825
+ body = void 0;
2826
+ for (const name of [
2827
+ "Content-Encoding",
2828
+ "Content-Language",
2829
+ "Content-Length",
2830
+ "Content-Location",
2831
+ "Content-Type"
2832
+ ]) headers.delete(name);
2833
+ }
2834
+ request = new Request(url, {
2835
+ method,
2836
+ headers,
2837
+ body,
2838
+ redirect: "manual"
2839
+ });
2840
+ }
2841
+ }
2769
2842
  /**
2770
2843
  * An error that is thrown when an activity fails to send to a remote inbox.
2771
2844
  * It contains structured information about the failure, including the HTTP
@@ -3272,6 +3345,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3272
3345
  }
3273
3346
  try {
3274
3347
  await sendActivity({
3348
+ allowPrivateAddress: this.allowPrivateAddress,
3275
3349
  keys,
3276
3350
  activity: message.activity,
3277
3351
  activityId: message.activityId,
@@ -3666,6 +3740,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3666
3740
  });
3667
3741
  const promises = [];
3668
3742
  for (const inbox in inboxes) promises.push(sendActivity({
3743
+ allowPrivateAddress: this.allowPrivateAddress,
3669
3744
  keys,
3670
3745
  activity: jsonLd,
3671
3746
  activityId: activity.id?.href,
@@ -4848,6 +4923,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
4848
4923
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
4849
4924
  const promises = [];
4850
4925
  for (const inbox in inboxes) promises.push(sendActivity({
4926
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
4851
4927
  keys,
4852
4928
  activity: ctx.activity,
4853
4929
  activityId: ctx.activityId,
@@ -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-DeYUdFal.mjs";
6
- import { t as formatAcceptSignature } from "./accept-CPkZzmGN.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-B4kUuy3o.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-79lZeYEo.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-CXH2XqYP.mjs";
10
- import { n as kvCache } from "./kv-cache-DYsF2MhP.mjs";
11
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-2R9vZ_ym.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CqYsVXcd.mjs";
13
- import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-CfUItkGW.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BzTt6nlk.mjs";
15
- import { t as getNodeInfo } from "./client-dx126nMS.mjs";
16
- import { t as nodeInfoToJson } from "./types-J53Kw7so.mjs";
17
- import { t as FederationBuilderImpl } from "./builder-TJzcpmDm.mjs";
18
- import { t as buildCollectionSynchronizationHeader } from "./collection-D-HqUuA2.mjs";
19
- import { t as KvKeyCache } from "./keycache-EGATflN-.mjs";
20
- import { t as acceptsJsonLd } from "./negotiation-SQvQgUqe.mjs";
21
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-BYCnXIGk.mjs";
22
- import { t as createExponentialBackoffPolicy } from "./retry-v_sGLH1d.mjs";
23
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CZwOs7XD.mjs";
5
+ import { n as version, t as name } from "./deno-CV4wsQzy.mjs";
6
+ import { t as formatAcceptSignature } from "./accept-Dd__NiUL.mjs";
7
+ import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-DiziIx9e.mjs";
8
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-C9MzFrOj.mjs";
9
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-DV9F08Dn.mjs";
10
+ import { n as kvCache } from "./kv-cache-FsT1ljhs.mjs";
11
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-BAeuDKjx.mjs";
12
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-D08_JUSb.mjs";
13
+ import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-Dc2OXyB-.mjs";
14
+ import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-_S-wFUFM.mjs";
15
+ import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-CQ7zd8tW.mjs";
16
+ import { t as nodeInfoToJson } from "./types-s4mCTGkT.mjs";
17
+ import { t as FederationBuilderImpl } from "./builder-Cwgo1nK5.mjs";
18
+ import { t as buildCollectionSynchronizationHeader } from "./collection-DnoYNwBC.mjs";
19
+ import { t as KvKeyCache } from "./keycache-C1aVprwy.mjs";
20
+ import { t as acceptsJsonLd } from "./negotiation-BHtZN6Y1.mjs";
21
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CX_L28b-.mjs";
22
+ import { t as createExponentialBackoffPolicy } from "./retry-bMXBL97A.mjs";
23
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-DmcKKipr.mjs";
24
24
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
25
25
  import { lookupWebFinger } from "@fedify/webfinger";
26
26
  import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
@@ -824,8 +824,16 @@ async function handleInboxInternal(request, parameters, span) {
824
824
  }
825
825
  let json;
826
826
  try {
827
- json = await request.clone().json();
827
+ json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
828
828
  } catch (error) {
829
+ if (error instanceof BodyTooLargeError) {
830
+ request.body?.cancel(error).catch(() => {});
831
+ span.setStatus({
832
+ code: SpanStatusCode.ERROR,
833
+ message: String(error)
834
+ });
835
+ return new Response("Inbox body too large.", { status: 413 });
836
+ }
829
837
  logger.error("Failed to parse JSON:\n{error}", {
830
838
  recipient,
831
839
  error
@@ -2159,6 +2167,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2159
2167
  }
2160
2168
  try {
2161
2169
  await sendActivity({
2170
+ allowPrivateAddress: this.allowPrivateAddress,
2162
2171
  keys,
2163
2172
  activity: message.activity,
2164
2173
  activityId: message.activityId,
@@ -2553,6 +2562,7 @@ var FederationImpl = class extends FederationBuilderImpl {
2553
2562
  });
2554
2563
  const promises = [];
2555
2564
  for (const inbox in inboxes) promises.push(sendActivity({
2565
+ allowPrivateAddress: this.allowPrivateAddress,
2556
2566
  keys,
2557
2567
  activity: jsonLd,
2558
2568
  activityId: activity.id?.href,
@@ -3735,6 +3745,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
3735
3745
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
3736
3746
  const promises = [];
3737
3747
  for (const inbox in inboxes) promises.push(sendActivity({
3748
+ allowPrivateAddress: ctx.federation.allowPrivateAddress,
3738
3749
  keys,
3739
3750
  activity: ctx.activity,
3740
3751
  activityId: ctx.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-D5NE6PkU.mjs";
4
+ import { n as FederationImpl } from "./middleware-CkJOjZjS.mjs";
5
5
  export { FederationImpl };