@fedify/fedify 2.4.0-dev.2200 → 2.4.0-dev.2219

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 (103) hide show
  1. package/dist/audience-CHQ-FlLD.mjs +95 -0
  2. package/dist/{builder-BGSNMnVH.mjs → builder-C5rYBZ16.mjs} +10 -2
  3. package/dist/compat/mod.d.cts +1 -1
  4. package/dist/compat/mod.d.ts +1 -1
  5. package/dist/compat/public-audience.test.mjs +1 -1
  6. package/dist/compat/transformers.test.mjs +1 -1
  7. package/dist/{compound-proof-Zpg7Fbk9.mjs → compound-proof-D0xY7MBs.mjs} +2 -2
  8. package/dist/{context-B2rlkkyJ.mjs → context-8Tu9kea0.mjs} +2 -0
  9. package/dist/{context-CDcAgm4l.d.cts → context-B6X-t8gI.d.cts} +81 -2
  10. package/dist/{context-9T--NSo4.d.ts → context-CtI307_A.d.ts} +81 -2
  11. package/dist/{deno-CxoV48e0.mjs → deno-Dj1TFhf4.mjs} +1 -1
  12. package/dist/{docloader-U141HKIl.mjs → docloader-CzyJe90o.mjs} +3 -3
  13. package/dist/federation/audience.test.d.mts +2 -0
  14. package/dist/federation/audience.test.mjs +130 -0
  15. package/dist/federation/builder.test.mjs +37 -1
  16. package/dist/federation/compound-fanout.test.mjs +4 -4
  17. package/dist/federation/handler-compound-proof.test.mjs +5 -5
  18. package/dist/federation/handler.test.mjs +7 -7
  19. package/dist/federation/hashlink-media.test.mjs +3 -3
  20. package/dist/federation/idempotency.test.mjs +2 -2
  21. package/dist/federation/metrics.test.mjs +2 -2
  22. package/dist/federation/middleware.test.mjs +125 -9
  23. package/dist/federation/mod.cjs +1 -1
  24. package/dist/federation/mod.d.cts +4 -4
  25. package/dist/federation/mod.d.ts +4 -4
  26. package/dist/federation/mod.js +1 -1
  27. package/dist/federation/outgoing-proof.test.mjs +6 -6
  28. package/dist/federation/portable-actor.test.mjs +4 -4
  29. package/dist/federation/portable-chain.test.mjs +4 -4
  30. package/dist/federation/portable-collection.test.mjs +8 -6
  31. package/dist/federation/portable-delivery.test.d.mts +2 -0
  32. package/dist/federation/portable-delivery.test.mjs +746 -0
  33. package/dist/federation/portable-gateway-keys.test.mjs +6 -6
  34. package/dist/federation/portable-inbox.test.mjs +5 -5
  35. package/dist/federation/portable-parse-uri.test.mjs +2 -2
  36. package/dist/federation/portable-route-activity.test.mjs +3 -3
  37. package/dist/federation/portable.test.mjs +234 -19
  38. package/dist/federation/send.test.mjs +4 -4
  39. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  40. package/dist/federation/tasks/tasks.test.mjs +3 -3
  41. package/dist/federation/temporal.test.mjs +1 -1
  42. package/dist/federation/webfinger.test.mjs +3 -3
  43. package/dist/{federation-Ij8VLHpy.cjs → federation-DC1yfb-8.cjs} +696 -41
  44. package/dist/{http-D8XakCKn.cjs → http-Cuk0w0rp.cjs} +1 -1
  45. package/dist/{http-BQq1Uldg.mjs → http-D-DpP4sd.mjs} +5 -5
  46. package/dist/{http-CshNXXui.js → http-Dx7yMmfw.js} +1 -1
  47. package/dist/{key-DYgoirTo.mjs → key-Djyt1BFx.mjs} +2 -2
  48. package/dist/{kv-BhNbN8Ed.d.cts → kv-B2mzK5tl.d.cts} +3 -2
  49. package/dist/{kv-BhNbN8Ed.d.ts → kv-B2mzK5tl.d.ts} +3 -2
  50. package/dist/{kv-cache-f_GNq0e7.cjs → kv-cache-BmLTVLXc.cjs} +1 -1
  51. package/dist/{kv-cache-Bpnt_Cck.js → kv-cache-CUZOCSKZ.js} +1 -1
  52. package/dist/{kv-cache-D0Mn-VQW.mjs → kv-cache-ecUXfCqW.mjs} +1 -1
  53. package/dist/{ld-eIekvNtm.mjs → ld-CNVWdooi.mjs} +3 -3
  54. package/dist/{metrics-BVpdjsaR.mjs → metrics-BnYleVpg.mjs} +1 -1
  55. package/dist/{middleware-CS3H_4tQ.js → middleware-BdzcazaN.js} +698 -43
  56. package/dist/{middleware-rpYJG85a.mjs → middleware-Bh592bk8.mjs} +1 -1
  57. package/dist/{middleware-BbTJ6C1-.mjs → middleware-C2_LcbL3.mjs} +431 -50
  58. package/dist/{mod-C5Mwx5ty.d.ts → mod-B8MI1mpi.d.ts} +1 -1
  59. package/dist/{mod-B3OBLj5g.d.ts → mod-BhTaDR5H.d.ts} +1 -1
  60. package/dist/{mod-D58KCqUg.d.cts → mod-C7_2j7VT.d.cts} +1 -1
  61. package/dist/{mod-Dp3Ij7sw.d.cts → mod-DFL6IxYQ.d.cts} +1 -1
  62. package/dist/mod.cjs +4 -4
  63. package/dist/mod.d.cts +6 -6
  64. package/dist/mod.d.ts +6 -6
  65. package/dist/mod.js +4 -4
  66. package/dist/nodeinfo/client.test.mjs +1 -1
  67. package/dist/nodeinfo/handler.test.mjs +2 -2
  68. package/dist/otel/mod.d.cts +1 -1
  69. package/dist/otel/mod.d.ts +1 -1
  70. package/dist/{owner-DpxyMeo9.mjs → owner-D_BDPwm2.mjs} +3 -3
  71. package/dist/{portable-collection-BvJ5om91.js → portable-collection-C0c0rUN9.js} +1 -1
  72. package/dist/{portable-collection-DyJwtPz1.cjs → portable-collection-DwJ8YJP2.cjs} +1 -1
  73. package/dist/{portable-collection-DOL3l2C9.mjs → portable-collection-hcONGyUs.mjs} +2 -2
  74. package/dist/{portable-key-B8kTV4n8.mjs → portable-key-B880yVlC.mjs} +1 -1
  75. package/dist/{proof-B9DEHHiO.mjs → proof-z6Prgi5I.mjs} +4 -4
  76. package/dist/{send-CZckda2X.mjs → send-Cw545L_2.mjs} +191 -10
  77. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  78. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  79. package/dist/sig/compound-proof-verification.test.mjs +3 -3
  80. package/dist/sig/compound-proof.test.mjs +3 -3
  81. package/dist/sig/http.test.mjs +3 -3
  82. package/dist/sig/key-owner-evidence.test.mjs +2 -2
  83. package/dist/sig/key.test.mjs +2 -2
  84. package/dist/sig/ld.test.mjs +2 -2
  85. package/dist/sig/mod.cjs +2 -2
  86. package/dist/sig/mod.js +2 -2
  87. package/dist/sig/owner.test.mjs +1 -1
  88. package/dist/sig/portable-collection.test.mjs +2 -2
  89. package/dist/sig/portable-key-id.test.mjs +2 -2
  90. package/dist/sig/portable-key.test.mjs +7 -7
  91. package/dist/sig/proof.test.mjs +4 -4
  92. package/dist/{temporal-Cc1ymEup.mjs → temporal-Cy-8WCod.mjs} +1 -1
  93. package/dist/testing/kv.test.mjs +1 -1
  94. package/dist/testing/mod.d.mts +82 -2
  95. package/dist/testing/mod.mjs +2 -2
  96. package/dist/{testing-C1itIjqW.mjs → testing-t8xDeJZ5.mjs} +1 -1
  97. package/dist/utils/docloader.test.mjs +6 -6
  98. package/dist/utils/kv-cache.test.mjs +2 -2
  99. package/dist/utils/mod.cjs +1 -1
  100. package/dist/utils/mod.d.cts +1 -1
  101. package/dist/utils/mod.d.ts +1 -1
  102. package/dist/utils/mod.js +1 -1
  103. package/package.json +7 -7
@@ -1,29 +1,30 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-CxoV48e0.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-BVpdjsaR.mjs";
6
- import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
7
4
  import { d as isPortableUri, f as isSameObjectId, i as getPortableDid, l as isPortableId, n as getCanonicalPortableId, o as hasPortableActor, r as getGatewayKeyBase, s as isCompatibleKeyId, t as getAuthenticationOrigin, u as isPortableKeyId } from "./portable-key-id-Ca30-hOa.mjs";
8
- import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-DYgoirTo.mjs";
5
+ import { n as isPubliclyAddressedNode, t as isInAudience } from "./audience-CHQ-FlLD.mjs";
6
+ import { n as version, t as name } from "./deno-Dj1TFhf4.mjs";
7
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-BnYleVpg.mjs";
8
+ import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
9
+ import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-Djyt1BFx.mjs";
9
10
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
10
- 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-eIekvNtm.mjs";
11
- import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-B9DEHHiO.mjs";
12
- import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-BQq1Uldg.mjs";
13
- import { t as getAuthenticatedDocumentLoader } from "./docloader-U141HKIl.mjs";
14
- import { n as kvCache } from "./kv-cache-D0Mn-VQW.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-CNVWdooi.mjs";
12
+ import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-z6Prgi5I.mjs";
13
+ import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-D-DpP4sd.mjs";
14
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-CzyJe90o.mjs";
15
+ import { n as kvCache } from "./kv-cache-ecUXfCqW.mjs";
15
16
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-CEnNz8O7.mjs";
16
17
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
17
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DpxyMeo9.mjs";
18
- import { t as verifyPortableObject } from "./portable-collection-DOL3l2C9.mjs";
19
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-CZckda2X.mjs";
20
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-BGSNMnVH.mjs";
18
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-D_BDPwm2.mjs";
19
+ import { t as verifyPortableObject } from "./portable-collection-hcONGyUs.mjs";
20
+ import { a as getOrderingDestination, i as sendActivityThroughGateways, n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-Cw545L_2.mjs";
21
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-C5rYBZ16.mjs";
21
22
  import { t as CircuitBreaker } from "./circuit-breaker-XJ53md4S.mjs";
22
23
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
23
- import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, s as verifyServedPortableObjects, t as containsCompoundPortableObject } from "./compound-proof-Zpg7Fbk9.mjs";
24
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, s as verifyServedPortableObjects, t as containsCompoundPortableObject } from "./compound-proof-D0xY7MBs.mjs";
24
25
  import { t as KvKeyCache } from "./keycache-Da8A8mZa.mjs";
25
26
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
26
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Cc1ymEup.mjs";
27
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Cy-8WCod.mjs";
27
28
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
28
29
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
29
30
  import { getLogger, withContext } from "@logtape/logtape";
@@ -31,10 +32,10 @@ import { RouterError } from "@fedify/uri-template";
31
32
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
32
33
  import { lookupWebFinger } from "@fedify/webfinger";
33
34
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
34
- import { uniq } from "es-toolkit";
35
35
  import { FetchError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, resolveDocumentLoaderTimeout, toCompatibleEf61Id } from "@fedify/vocab-runtime";
36
- 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";
37
36
  import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
37
+ import { uniq } from "es-toolkit";
38
+ 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";
38
39
  import jsonld from "@fedify/vocab-runtime/jsonld";
39
40
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
40
41
  import { domainToASCII } from "node:url";
@@ -1212,6 +1213,15 @@ async function handleObject(request, { values, context, objectDispatcher, author
1212
1213
  * only if its ID canonically matches the requested portable ID and it
1213
1214
  * satisfies the FEP-ef61 proof policy.
1214
1215
  *
1216
+ * FEP-ef61 forbids serving a non-public object to anyone but its intended
1217
+ * audience. If the dispatcher has an authorization predicate, it decides who
1218
+ * may retrieve the object, and the response is marked with
1219
+ * `Cache-Control: private`, as it depends on the requester. Otherwise, only
1220
+ * a publicly addressed object is served, and any other object is responded
1221
+ * to with `404 Not Found`, as if this server did not store it. Actors, and
1222
+ * anything the actor dispatcher returns, are exempt from the latter, as
1223
+ * actors have no audience.
1224
+ *
1215
1225
  * A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
1216
1226
  * Proof made with a key of the DID in its ID. An unsigned tombstone cannot
1217
1227
  * be served as a portable object, so the response is the same as for
@@ -1221,7 +1231,7 @@ async function handleObject(request, { values, context, objectDispatcher, author
1221
1231
  * @param parameters The parameters for handling the portable object.
1222
1232
  * @returns A promise that resolves to an HTTP response.
1223
1233
  */
1224
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
1234
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, kind, onNotFound, onUnauthorized }) {
1225
1235
  const logger = getLogger([
1226
1236
  "fedify",
1227
1237
  "federation",
@@ -1240,14 +1250,32 @@ async function handlePortableObject(request, { values, context, objectDispatcher
1240
1250
  if (authorizePredicate != null) {
1241
1251
  if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
1242
1252
  }
1253
+ const contextLoader = getNormalizationContextLoader(context.contextLoader);
1243
1254
  let jsonLd;
1244
- let servedId;
1245
- let result;
1255
+ let root;
1246
1256
  try {
1247
1257
  jsonLd = await object.toJsonLd(context);
1248
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
1258
+ root = await expandJsonLdRoot(jsonLd, contextLoader);
1259
+ } catch (error) {
1260
+ logger.error("Failed to serialize the portable object {portableId}:\n{error}", {
1261
+ portableId: canonicalId,
1262
+ error
1263
+ });
1264
+ return portableObjectInternalServerError(request);
1265
+ }
1266
+ const servedId = root?.["@id"];
1267
+ if (root == null || typeof servedId !== "string" || !isRequestedPortableId(servedId, canonicalId)) {
1268
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
1269
+ return await onNotFound(request);
1270
+ }
1271
+ if (authorizePredicate == null && kind === "object" && !isActor(object) && !isPubliclyAddressedNode(root)) {
1272
+ logger.debug("Not serving the portable object {portableId}, as it is not publicly addressed and its object dispatcher has no authorization predicate; responding as if this server did not store the object. Set an authorization predicate that checks if the request is signed by an actor in the object's audience to serve it.", { portableId: canonicalId });
1273
+ return await onNotFound(request);
1274
+ }
1275
+ let result;
1276
+ try {
1249
1277
  result = await verifyPortableObjectProof(jsonLd, {
1250
- contextLoader: context.contextLoader,
1278
+ contextLoader,
1251
1279
  documentLoader: context.documentLoader,
1252
1280
  tracerProvider: context.tracerProvider,
1253
1281
  meterProvider: context.meterProvider
@@ -1259,10 +1287,6 @@ async function handlePortableObject(request, { values, context, objectDispatcher
1259
1287
  });
1260
1288
  return portableObjectInternalServerError(request);
1261
1289
  }
1262
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
1263
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
1264
- return await onNotFound(request);
1265
- }
1266
1290
  const tombstone = object instanceof Tombstone;
1267
1291
  if (!result.verified) {
1268
1292
  if (tombstone && result.reason.type === "missingProof") {
@@ -1281,25 +1305,26 @@ async function handlePortableObject(request, { values, context, objectDispatcher
1281
1305
  return portableObjectInternalServerError(request);
1282
1306
  }
1283
1307
  }
1308
+ const headers = new Headers({
1309
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
1310
+ Vary: "Accept"
1311
+ });
1312
+ if (authorizePredicate != null) headers.set("Cache-Control", "private");
1284
1313
  return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
1285
1314
  status: tombstone ? 410 : 200,
1286
- headers: {
1287
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
1288
- Vary: "Accept"
1289
- }
1315
+ headers
1290
1316
  });
1291
1317
  }
1292
1318
  function isRequestedPortableObject(object, canonicalId) {
1293
1319
  return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
1294
1320
  }
1295
- async function getJsonLdRootId(jsonLd, contextLoader) {
1321
+ async function expandJsonLdRoot(jsonLd, contextLoader) {
1296
1322
  const expanded = await jsonld.expand(jsonLd, {
1297
- documentLoader: getNormalizationContextLoader(contextLoader),
1323
+ documentLoader: contextLoader,
1298
1324
  keepFreeFloatingNodes: true
1299
1325
  });
1300
1326
  if (expanded.length !== 1) return null;
1301
- const id = expanded[0]["@id"];
1302
- return typeof id === "string" ? id : null;
1327
+ return expanded[0];
1303
1328
  }
1304
1329
  function isRequestedPortableId(id, canonicalId) {
1305
1330
  if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
@@ -4045,6 +4070,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
4045
4070
  function maxDelay(first, second) {
4046
4071
  return Temporal.Duration.compare(first, second) >= 0 ? first : second;
4047
4072
  }
4073
+ /**
4074
+ * Gets the fields of a queued message that describe the FEP-ef61 portable
4075
+ * inbox an inbox stands for, if any, omitting the absent ones.
4076
+ */
4077
+ function getPortableMessageFields(portableInbox, gatewayInboxes) {
4078
+ return {
4079
+ ...portableInbox == null ? {} : { portableInbox },
4080
+ ...gatewayInboxes == null ? {} : { gatewayInboxes: [...gatewayInboxes] }
4081
+ };
4082
+ }
4083
+ /**
4084
+ * Sends an activity to an inbox, or, if the inbox is the compatible
4085
+ * identifier of an FEP-ef61 portable inbox on more than one gateway, through
4086
+ * the first gateway that accepts it.
4087
+ */
4088
+ function sendActivityToInbox(inbox, gatewayInboxes, parameters) {
4089
+ if (gatewayInboxes == null || gatewayInboxes.length < 2) return sendActivity({
4090
+ ...parameters,
4091
+ inbox: new URL(inbox)
4092
+ });
4093
+ return sendActivityThroughGateways(parameters, gatewayInboxes.map((i) => new URL(i)));
4094
+ }
4095
+ function parseInstant(value) {
4096
+ if (value == null) return void 0;
4097
+ try {
4098
+ return Temporal.Instant.from(value);
4099
+ } catch (error) {
4100
+ if (error instanceof RangeError) return void 0;
4101
+ throw error;
4102
+ }
4103
+ }
4048
4104
  function isTransportDeliveryError(error) {
4049
4105
  return error instanceof FetchError || isAbortError(error);
4050
4106
  }
@@ -4625,6 +4681,20 @@ var FederationImpl = class extends FederationBuilderImpl {
4625
4681
  }
4626
4682
  recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
4627
4683
  };
4684
+ const { gatewayInboxes } = message;
4685
+ if (gatewayInboxes != null && gatewayInboxes.length > 1) {
4686
+ await this.#listenGatewayOutboxMessage(_, {
4687
+ ...message,
4688
+ gatewayInboxes
4689
+ }, span, {
4690
+ keys,
4691
+ logData,
4692
+ getActorIds,
4693
+ parseActivity,
4694
+ dropHeldOutboxMessage
4695
+ });
4696
+ return;
4697
+ }
4628
4698
  try {
4629
4699
  const inbox = new URL(message.inbox);
4630
4700
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
@@ -4847,6 +4917,308 @@ var FederationImpl = class extends FederationBuilderImpl {
4847
4917
  }
4848
4918
  logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
4849
4919
  }
4920
+ /**
4921
+ * Delivers a queued activity to an FEP-ef61 portable inbox through the first
4922
+ * of its gateways that accepts it, trying them one after another in a single
4923
+ * round, so that the round counts as one attempt for the retry policy, keeps
4924
+ * the message's place in its ordering key, and is replayed as a whole by
4925
+ * queues that retry natively.
4926
+ *
4927
+ * A gateway that fails with a permanent failure status is not tried again.
4928
+ * A gateway whose circuit is open, or which asked to be retried later with
4929
+ * `Retry-After`, is skipped in this round. If no gateway accepts the
4930
+ * activity, the message is held while every remaining gateway is held, and
4931
+ * otherwise retried according to the retry policy.
4932
+ */
4933
+ async #listenGatewayOutboxMessage(contextData, message, span, helpers) {
4934
+ const logger = getLogger([
4935
+ "fedify",
4936
+ "federation",
4937
+ "outbox"
4938
+ ]);
4939
+ const circuitLogger = getLogger([
4940
+ "fedify",
4941
+ "federation",
4942
+ "circuit"
4943
+ ]);
4944
+ const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
4945
+ const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
4946
+ const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
4947
+ const survivors = [];
4948
+ let lastError;
4949
+ let lastPermanent;
4950
+ let activity;
4951
+ const getActivity = async () => activity ??= await parseActivity();
4952
+ const drop = async (circuit, remoteHost, inbox, heldSince) => {
4953
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
4954
+ };
4955
+ const heldSince = parseInstant(message.circuitHeldSince);
4956
+ if (circuit != null && heldSince != null && Temporal.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal.Now.instant()) <= 0) {
4957
+ const inbox = new URL(message.gatewayInboxes[0]);
4958
+ await drop(circuit, getRemoteHost(inbox), inbox, heldSince);
4959
+ return;
4960
+ }
4961
+ for (const inboxHref of message.gatewayInboxes) {
4962
+ const inbox = new URL(inboxHref);
4963
+ const remoteHost = getRemoteHost(inbox);
4964
+ const now = Temporal.Now.instant();
4965
+ const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
4966
+ if (notBefore != null && Temporal.Instant.compare(notBefore, now) > 0) {
4967
+ survivors.push({
4968
+ inbox: inboxHref,
4969
+ holdDelay: now.until(notBefore),
4970
+ notBefore
4971
+ });
4972
+ continue;
4973
+ }
4974
+ let decision;
4975
+ if (circuit != null) try {
4976
+ decision = await circuit.beforeSend(remoteHost, message);
4977
+ } catch (circuitError) {
4978
+ circuitLogger.error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
4979
+ ...logData,
4980
+ remoteHost,
4981
+ error: circuitError
4982
+ });
4983
+ }
4984
+ if (decision != null && circuit != null) {
4985
+ if (decision.type === "drop") {
4986
+ await drop(circuit, remoteHost, inbox, decision.heldSince);
4987
+ return;
4988
+ } else if (decision.type === "hold") {
4989
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
4990
+ survivors.push({
4991
+ inbox: inboxHref,
4992
+ holdDelay: decision.delay,
4993
+ circuitHeld: true
4994
+ });
4995
+ continue;
4996
+ } else if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
4997
+ }
4998
+ try {
4999
+ await sendActivity({
5000
+ allowPrivateAddress: this.allowPrivateAddress,
5001
+ keys,
5002
+ activity: message.activity,
5003
+ activityId: message.activityId,
5004
+ activityType: message.activityType,
5005
+ inbox,
5006
+ sharedInbox: message.sharedInbox,
5007
+ headers: new Headers(message.headers),
5008
+ specDeterminer,
5009
+ meterProvider: this.meterProvider,
5010
+ tracerProvider: this.tracerProvider
5011
+ });
5012
+ } catch (error) {
5013
+ lastError = error;
5014
+ const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
5015
+ let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
5016
+ let retryAt;
5017
+ if (retryAfter != null) try {
5018
+ retryAt = Temporal.Now.instant().add(retryAfter);
5019
+ } catch (e) {
5020
+ if (!(e instanceof RangeError)) throw e;
5021
+ retryAfter = void 0;
5022
+ }
5023
+ let postDecision;
5024
+ if (circuit != null) try {
5025
+ if (error instanceof SendActivityError) {
5026
+ const { statusCode } = error;
5027
+ const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await circuit.recordReachableFailure(remoteHost) : statusCode >= 500 ? await circuit.recordFailure(remoteHost) : void 0;
5028
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
5029
+ } else if (isTransportDeliveryError(error)) {
5030
+ const stateChange = await circuit.recordFailure(remoteHost);
5031
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
5032
+ }
5033
+ if (!isPermanentFailure) postDecision = await circuit.beforeSend(remoteHost, message);
5034
+ } catch (circuitError) {
5035
+ circuitLogger.error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
5036
+ ...logData,
5037
+ remoteHost,
5038
+ error: circuitError
5039
+ });
5040
+ }
5041
+ span.addEvent("activitypub.delivery.failed", {
5042
+ "activitypub.remote.host": remoteHost,
5043
+ "activitypub.delivery.attempt": message.attempt,
5044
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
5045
+ ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
5046
+ });
5047
+ try {
5048
+ await this.onOutboxError?.(error, await getActivity());
5049
+ } catch (handlerError) {
5050
+ logger.error("An unexpected error occurred in onError handler:\n{error}", {
5051
+ ...logData,
5052
+ inbox: inboxHref,
5053
+ error: handlerError
5054
+ });
5055
+ }
5056
+ if (isPermanentFailure) {
5057
+ logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
5058
+ ...logData,
5059
+ inbox: inboxHref,
5060
+ status: error.statusCode
5061
+ });
5062
+ lastPermanent = {
5063
+ error,
5064
+ inbox
5065
+ };
5066
+ continue;
5067
+ }
5068
+ if (postDecision?.type === "drop" && circuit != null) {
5069
+ await drop(circuit, remoteHost, inbox, postDecision.heldSince);
5070
+ return;
5071
+ } else if (postDecision?.type === "hold") {
5072
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, postDecision.state);
5073
+ survivors.push({
5074
+ inbox: inboxHref,
5075
+ holdDelay: retryAfter == null ? postDecision.delay : maxDelay(postDecision.delay, retryAfter),
5076
+ circuitHeld: true,
5077
+ postFailure: true,
5078
+ notBefore: retryAt,
5079
+ retryAfter
5080
+ });
5081
+ } else survivors.push({
5082
+ inbox: inboxHref,
5083
+ notBefore: retryAt,
5084
+ retryAfter
5085
+ });
5086
+ logger.warn("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); trying the next gateway if any:\n{error}", {
5087
+ ...logData,
5088
+ inbox: inboxHref,
5089
+ error
5090
+ });
5091
+ continue;
5092
+ }
5093
+ if (circuit != null) try {
5094
+ const stateChange = await circuit.recordSuccess(remoteHost);
5095
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
5096
+ } catch (error) {
5097
+ circuitLogger.error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
5098
+ ...logData,
5099
+ remoteHost,
5100
+ error
5101
+ });
5102
+ }
5103
+ logger.info("Successfully sent activity {activityId} to {inbox}.", {
5104
+ ...logData,
5105
+ inbox: inboxHref
5106
+ });
5107
+ return;
5108
+ }
5109
+ if (lastError != null) span.setStatus({
5110
+ code: SpanStatusCode.ERROR,
5111
+ message: String(lastError)
5112
+ });
5113
+ if (survivors.length < 1) {
5114
+ const { error, inbox } = lastPermanent;
5115
+ this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
5116
+ logger.warn("Permanent delivery failure for activity {activityId} to every gateway of {portableInbox}; not retrying.", {
5117
+ ...logData,
5118
+ portableInbox: message.portableInbox
5119
+ });
5120
+ if (this.outboxPermanentFailureHandler != null) {
5121
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
5122
+ try {
5123
+ await this.outboxPermanentFailureHandler(ctx, {
5124
+ reason: "http",
5125
+ inbox,
5126
+ activity: await getActivity(),
5127
+ error,
5128
+ statusCode: error.statusCode,
5129
+ actorIds: getActorIds()
5130
+ });
5131
+ } catch (handlerError) {
5132
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
5133
+ ...logData,
5134
+ error: handlerError
5135
+ });
5136
+ }
5137
+ }
5138
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
5139
+ return;
5140
+ }
5141
+ const now = Temporal.Now.instant();
5142
+ const gatewayNotBefore = {};
5143
+ for (const survivor of survivors) if (survivor.notBefore != null && Temporal.Instant.compare(survivor.notBefore, now) > 0) gatewayNotBefore[survivor.inbox] = survivor.notBefore.toString();
5144
+ const { circuitHeld: _circuitHeld, gatewayNotBefore: _gatewayNotBefore, ...rest } = message;
5145
+ const nextMessage = {
5146
+ ...rest,
5147
+ inbox: survivors[0].inbox,
5148
+ gatewayInboxes: survivors.map((s) => s.inbox),
5149
+ ...globalThis.Object.keys(gatewayNotBefore).length < 1 ? {} : { gatewayNotBefore }
5150
+ };
5151
+ const { outboxQueue } = this;
5152
+ const policyDelay = this.outboxRetryPolicy({
5153
+ elapsedTime: Temporal.Instant.from(message.started).until(now),
5154
+ attempts: message.attempt
5155
+ });
5156
+ if (outboxQueue != null && survivors.every((s) => s.holdDelay != null) && (policyDelay != null || !survivors.some((s) => s.postFailure))) {
5157
+ let delay = survivors.map((s) => s.holdDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
5158
+ let heldMessage = nextMessage;
5159
+ if (survivors.some((s) => s.circuitHeld)) {
5160
+ const since = heldSince ?? now;
5161
+ if (circuit != null) delay = circuit.capHeldDelay(since, delay);
5162
+ heldMessage = {
5163
+ ...nextMessage,
5164
+ circuitHeld: true,
5165
+ circuitHeldSince: since.toString()
5166
+ };
5167
+ }
5168
+ logger.warn("Failed to send activity {activityId} to {portableInbox}; holding it until one of its gateways can be tried again.", {
5169
+ ...logData,
5170
+ portableInbox: message.portableInbox
5171
+ });
5172
+ await outboxQueue.enqueue(heldMessage, {
5173
+ delay: clampNegativeDelay(delay),
5174
+ orderingKey: message.orderingKey
5175
+ });
5176
+ this.metrics.recordQueueTaskEnqueued({
5177
+ role: "outbox",
5178
+ queue: outboxQueue,
5179
+ activityType: heldMessage.activityType
5180
+ }, heldMessage.attempt);
5181
+ return;
5182
+ }
5183
+ const hasRetryAfter = survivors.some((s) => s.retryAfter != null);
5184
+ if (outboxQueue?.nativeRetrial && !hasRetryAfter && survivors.length === message.gatewayInboxes.length && globalThis.Object.keys(gatewayNotBefore).length < 1) {
5185
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox}; backend will handle retry:\n{error}", {
5186
+ ...logData,
5187
+ portableInbox: message.portableInbox,
5188
+ error: lastError
5189
+ });
5190
+ throw lastError;
5191
+ }
5192
+ if (policyDelay == null || outboxQueue == null) {
5193
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
5194
+ ...logData,
5195
+ portableInbox: message.portableInbox,
5196
+ error: lastError
5197
+ });
5198
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
5199
+ return;
5200
+ }
5201
+ const delay = survivors.map((s) => s.holdDelay ?? s.retryAfter ?? policyDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
5202
+ const retryMessage = {
5203
+ ...nextMessage,
5204
+ attempt: message.attempt + 1
5205
+ };
5206
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} (attempt #{attempt}); retry...:\n{error}", {
5207
+ ...logData,
5208
+ portableInbox: message.portableInbox,
5209
+ error: lastError
5210
+ });
5211
+ await outboxQueue.enqueue(retryMessage, {
5212
+ delay: clampNegativeDelay(delay),
5213
+ orderingKey: message.orderingKey
5214
+ });
5215
+ this.metrics.recordQueueTaskEnqueued({
5216
+ role: "outbox",
5217
+ queue: outboxQueue,
5218
+ activityType: retryMessage.activityType
5219
+ }, retryMessage.attempt);
5220
+ recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
5221
+ }
4850
5222
  async #listenInboxMessage(ctxData, message, span, onActivityType) {
4851
5223
  const logger = getLogger([
4852
5224
  "fedify",
@@ -5276,13 +5648,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5276
5648
  activity: jsonLd
5277
5649
  });
5278
5650
  const promises = [];
5279
- for (const inbox in inboxes) promises.push(sendActivity({
5651
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
5280
5652
  allowPrivateAddress: this.allowPrivateAddress,
5281
5653
  keys,
5282
5654
  activity: jsonLd,
5283
5655
  activityId: activity.id?.href,
5284
5656
  activityType: getTypeId(activity).href,
5285
- inbox: new URL(inbox),
5286
5657
  sharedInbox: inboxes[inbox].sharedInbox,
5287
5658
  headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
5288
5659
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
@@ -5309,8 +5680,8 @@ var FederationImpl = class extends FederationBuilderImpl {
5309
5680
  propagation.inject(context.active(), carrier);
5310
5681
  const messages = [];
5311
5682
  for (const inbox in inboxes) {
5312
- const inboxOrigin = new URL(inbox).origin;
5313
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${inboxOrigin}`;
5683
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
5684
+ const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
5314
5685
  const message = {
5315
5686
  type: "outbox",
5316
5687
  id: crypto.randomUUID(),
@@ -5326,6 +5697,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5326
5697
  attempt: 0,
5327
5698
  headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
5328
5699
  orderingKey: messageOrderingKey,
5700
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
5329
5701
  traceContext: carrier
5330
5702
  };
5331
5703
  messages.push({
@@ -5685,10 +6057,10 @@ var FederationImpl = class extends FederationBuilderImpl {
5685
6057
  onNotFound
5686
6058
  });
5687
6059
  case "collection": {
5688
- const name = route.name.replace(/^collection:/, "");
6060
+ const name = this.getCollectionName(route.name);
5689
6061
  const callbacks = this.collectionCallbacks[name];
5690
6062
  return await handleCustomCollection(request, {
5691
- name,
6063
+ name: String(name),
5692
6064
  context,
5693
6065
  values: route.values,
5694
6066
  collectionCallbacks: callbacks,
@@ -5699,10 +6071,10 @@ var FederationImpl = class extends FederationBuilderImpl {
5699
6071
  });
5700
6072
  }
5701
6073
  case "orderedCollection": {
5702
- const name = route.name.replace(/^orderedCollection:/, "");
6074
+ const name = this.getCollectionName(route.name);
5703
6075
  const callbacks = this.collectionCallbacks[name];
5704
6076
  return await handleOrderedCollection(request, {
5705
- name,
6077
+ name: String(name),
5706
6078
  context,
5707
6079
  values: route.values,
5708
6080
  collectionCallbacks: callbacks,
@@ -5775,6 +6147,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5775
6147
  objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
5776
6148
  authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
5777
6149
  canonicalId: portable.canonicalId,
6150
+ kind: "actor",
5778
6151
  onUnauthorized,
5779
6152
  onNotFound
5780
6153
  });
@@ -5795,6 +6168,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5795
6168
  objectDispatcher: callbacks?.dispatcher,
5796
6169
  authorizePredicate: callbacks?.authorizePredicate,
5797
6170
  canonicalId: portable.canonicalId,
6171
+ kind: "object",
5798
6172
  onUnauthorized,
5799
6173
  onNotFound
5800
6174
  });
@@ -6466,8 +6840,10 @@ var ContextImpl = class ContextImpl {
6466
6840
  const collectionRegex = new RegExp(`^(${["collection", "orderedCollection"].join("|")}):(.*)$`);
6467
6841
  const match = route.name.match(collectionRegex);
6468
6842
  if (match !== null) {
6469
- const [, type, name] = match;
6843
+ const [, type] = match;
6844
+ const name = this.federation.getCollectionName(route.name);
6470
6845
  const cls = this.federation.collectionTypeIds[name];
6846
+ if (cls == null) return null;
6471
6847
  return {
6472
6848
  type,
6473
6849
  name,
@@ -6791,9 +7167,10 @@ var ContextImpl = class ContextImpl {
6791
7167
  id: crypto.randomUUID(),
6792
7168
  baseUrl: this.origin,
6793
7169
  keys: keyJwkPairs,
6794
- inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
7170
+ inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
6795
7171
  actorIds: [...actorIds],
6796
- sharedInbox
7172
+ sharedInbox,
7173
+ ...getPortableMessageFields(portableInbox, gatewayInboxes)
6797
7174
  }])),
6798
7175
  activity: activityJsonLd,
6799
7176
  activityId: activity.id?.href,
@@ -6869,7 +7246,7 @@ var ContextImpl = class ContextImpl {
6869
7246
  "inbox"
6870
7247
  ]);
6871
7248
  const contextLoader = options.contextLoader ?? this.contextLoader;
6872
- const json = await activity.toJsonLd({ contextLoader });
7249
+ let json = await activity.toJsonLd({ contextLoader });
6873
7250
  const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
6874
7251
  documentLoader: this.documentLoader,
6875
7252
  contextLoader: this.contextLoader,
@@ -6931,6 +7308,7 @@ var ContextImpl = class ContextImpl {
6931
7308
  }
6932
7309
  logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
6933
7310
  activity = fetched;
7311
+ json = await fetched.toJsonLd({ contextLoader });
6934
7312
  } else logger.debug("Object Integrity Proofs are verified.", {
6935
7313
  recipient,
6936
7314
  activity: json
@@ -7090,6 +7468,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
7090
7468
  throw error;
7091
7469
  }
7092
7470
  }
7471
+ async isSignedByAudience(object, options = {}) {
7472
+ return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
7473
+ }
7093
7474
  };
7094
7475
  function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
7095
7476
  return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
@@ -7177,13 +7558,12 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
7177
7558
  if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
7178
7559
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
7179
7560
  const promises = [];
7180
- for (const inbox in inboxes) promises.push(sendActivity({
7561
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
7181
7562
  allowPrivateAddress: ctx.federation.allowPrivateAddress,
7182
7563
  keys,
7183
7564
  activity: ctx.activity,
7184
7565
  activityId: ctx.activityId,
7185
7566
  activityType: ctx.activityType,
7186
- inbox: new URL(inbox),
7187
7567
  sharedInbox: inboxes[inbox].sharedInbox,
7188
7568
  meterProvider: ctx.meterProvider,
7189
7569
  tracerProvider: ctx.tracerProvider,
@@ -7211,7 +7591,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
7211
7591
  const started = (/* @__PURE__ */ new Date()).toISOString();
7212
7592
  const messages = [];
7213
7593
  for (const inbox in inboxes) {
7214
- const inboxUrl = new URL(inbox);
7594
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
7215
7595
  const message = {
7216
7596
  type: "outbox",
7217
7597
  id: crypto.randomUUID(),
@@ -7226,7 +7606,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
7226
7606
  started,
7227
7607
  attempt: 0,
7228
7608
  headers: {},
7229
- orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
7609
+ orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
7610
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
7230
7611
  traceContext: carrier
7231
7612
  };
7232
7613
  messages.push({