@fedify/fedify 2.4.0-dev.2193 → 2.4.0-dev.2212

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 (95) hide show
  1. package/dist/audience-CHQ-FlLD.mjs +95 -0
  2. package/dist/{builder-0onlMY4u.mjs → builder-CKAVFvcH.mjs} +2 -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-B-Fk6Slo.mjs → compound-proof-BP5SOdXv.mjs} +2 -2
  8. package/dist/{context-B2rlkkyJ.mjs → context-8Tu9kea0.mjs} +2 -0
  9. package/dist/{context-CDcAgm4l.d.cts → context-Bl_Y7T_3.d.cts} +80 -1
  10. package/dist/{context-9T--NSo4.d.ts → context-DMd11IAu.d.ts} +80 -1
  11. package/dist/{deno-DB_Sh0LK.mjs → deno-BKp9fYB3.mjs} +1 -1
  12. package/dist/{docloader-DpWYYY6a.mjs → docloader-BjJreYQU.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 +1 -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 +9 -9
  23. package/dist/federation/mod.cjs +1 -1
  24. package/dist/federation/mod.d.cts +3 -3
  25. package/dist/federation/mod.d.ts +3 -3
  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-BsGo0EH2.cjs → federation-B-0EXZPH.cjs} +679 -35
  44. package/dist/{http-DQf9Mw4V.mjs → http--IGUT1mg.mjs} +5 -5
  45. package/dist/{http-BEivI0TS.cjs → http-ByBlnY3D.cjs} +1 -1
  46. package/dist/{http-BGmxtpo2.js → http-Di8yiDMY.js} +1 -1
  47. package/dist/{key-DA3Yx8p2.mjs → key-ChfV9C3v.mjs} +2 -2
  48. package/dist/{kv-cache-1hafSoqH.cjs → kv-cache-BDwUzxBJ.cjs} +1 -1
  49. package/dist/{kv-cache-OFBvrKh3.mjs → kv-cache-DFIUR-E5.mjs} +1 -1
  50. package/dist/{kv-cache-DduYhpee.js → kv-cache-DpwDPt9V.js} +1 -1
  51. package/dist/{ld-C15yzuY6.mjs → ld-DBBVabBC.mjs} +3 -3
  52. package/dist/{metrics-B1H23mlq.mjs → metrics-DPQKFkiJ.mjs} +1 -1
  53. package/dist/{middleware-BqqycC0P.mjs → middleware-BW7-tHAG.mjs} +422 -44
  54. package/dist/{middleware-BZQnf0nI.mjs → middleware-G9zpxUsD.mjs} +1 -1
  55. package/dist/{middleware-ClaUFaLb.js → middleware-VXGLiHtY.js} +681 -37
  56. package/dist/{mod-B3OBLj5g.d.ts → mod-BVNGHgVr.d.ts} +1 -1
  57. package/dist/{mod-D58KCqUg.d.cts → mod-Cyy1Wahy.d.cts} +1 -1
  58. package/dist/mod.cjs +4 -4
  59. package/dist/mod.d.cts +4 -4
  60. package/dist/mod.d.ts +4 -4
  61. package/dist/mod.js +4 -4
  62. package/dist/nodeinfo/client.test.mjs +1 -1
  63. package/dist/nodeinfo/handler.test.mjs +2 -2
  64. package/dist/{owner-BVpZBM-V.mjs → owner-BW5VsXG3.mjs} +3 -3
  65. package/dist/{portable-collection-BFSShzwh.cjs → portable-collection-CXcAlwei.cjs} +1 -1
  66. package/dist/{portable-collection-Dn2GdZdI.mjs → portable-collection-Ct99JsBe.mjs} +2 -2
  67. package/dist/{portable-collection-D535cSz_.js → portable-collection-FTPa5D91.js} +1 -1
  68. package/dist/{portable-key-Bmwmq7Y6.mjs → portable-key-U3lW06QJ.mjs} +1 -1
  69. package/dist/{proof-DrJWKeOl.mjs → proof-MFz2960e.mjs} +4 -4
  70. package/dist/{send-CT3a-0Vd.mjs → send-Duj8xys1.mjs} +191 -10
  71. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  72. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  73. package/dist/sig/compound-proof-verification.test.mjs +3 -3
  74. package/dist/sig/compound-proof.test.mjs +3 -3
  75. package/dist/sig/http.test.mjs +3 -3
  76. package/dist/sig/key-owner-evidence.test.mjs +2 -2
  77. package/dist/sig/key.test.mjs +2 -2
  78. package/dist/sig/ld.test.mjs +2 -2
  79. package/dist/sig/mod.cjs +2 -2
  80. package/dist/sig/mod.js +2 -2
  81. package/dist/sig/owner.test.mjs +1 -1
  82. package/dist/sig/portable-collection.test.mjs +2 -2
  83. package/dist/sig/portable-key-id.test.mjs +2 -2
  84. package/dist/sig/portable-key.test.mjs +7 -7
  85. package/dist/sig/proof.test.mjs +4 -4
  86. package/dist/{temporal-DNqluyEc.mjs → temporal-CVcPKB1N.mjs} +1 -1
  87. package/dist/testing/kv.test.mjs +1 -1
  88. package/dist/testing/mod.d.mts +79 -0
  89. package/dist/testing/mod.mjs +2 -2
  90. package/dist/{testing-C1itIjqW.mjs → testing-t8xDeJZ5.mjs} +1 -1
  91. package/dist/utils/docloader.test.mjs +6 -6
  92. package/dist/utils/kv-cache.test.mjs +2 -2
  93. package/dist/utils/mod.cjs +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +6 -6
@@ -2,16 +2,16 @@ import { Temporal } from "temporal-polyfill";
2
2
  import { URLPattern } from "urlpattern-polyfill";
3
3
  import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
4
4
  import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
5
- import { $ as instrumentDocumentLoader, A as hasSignature, At as name, C as InvalidContextReferenceError, Ct as hasPortableActor, Dt as isPortableUri, E as compactJsonLd, Et as isPortableKeyId, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, O as detachSignature, Ot as isSameObjectId, P as signJsonLd, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getRawCanonicalPortableId, Tt as isPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as getGatewayKeyBase, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as version, k as getNormalizationContextLoader, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as getAuthenticationOrigin, w as assertSafeJsonLd, wt as isCompatibleKeyId, xt as getPortableDid, y as verifyPortableObjectProofPolicy, yt as getCanonicalPortableId, z as exportJwk } from "./http-BGmxtpo2.js";
5
+ import { $ as instrumentDocumentLoader, A as hasSignature, At as name, C as InvalidContextReferenceError, Ct as hasPortableActor, Dt as isPortableUri, E as compactJsonLd, Et as isPortableKeyId, F as verifyCompactJsonLd, G as validateCryptoKey, M as isClearlyMalformedContextReference, N as isInvalidUrlTypeError, O as detachSignature, Ot as isSameObjectId, P as signJsonLd, Q as getRemoteHost, R as wrapContextLoaderForJsonLd, S as normalizeOutgoingActivityJsonLd, St as getRawCanonicalPortableId, Tt as isPortableId, W as importJwk, X as getDurationMs, Z as getFederationMetrics, _ as verifyObject, at as recordCollectionTotalItems, b as verifyPortableObjectProofWithRoot, bt as getGatewayKeyBase, c as verifyRequestDetailed, ct as recordInboxActivity, d as InvalidPortableObjectIdError, dt as recordWebFingerHandle, et as isAbortError, f as classifyFep2277CoreType, ft as registerQueueDepthGauge, g as verifyMapLocalProof, h as signObject, i as selectRequestSignature, it as recordCollectionRequest, j as hasSignatureLike, jt as version, k as getNormalizationContextLoader, lt as recordOutboxActivity, m as hasProofLike, mt as formatAcceptSignature, n as listRequestSignatures, nt as recordCollectionDispatchDuration, o as validateMaxSignatures, pt as preloadedOnlyDocumentLoader, q as MAX_PORTABLE_KEY_TTL, r as parseRfc9421SignatureInput, rt as recordCollectionPageItems, s as verifyRequest, st as recordFanoutRecipients, t as doubleKnock, tt as recordCircuitBreakerStateChange, ut as recordOutboxEnqueue, v as verifyPortableObjectProof, vt as getAuthenticationOrigin, w as assertSafeJsonLd, wt as isCompatibleKeyId, xt as getPortableDid, y as verifyPortableObjectProofPolicy, yt as getCanonicalPortableId, z as exportJwk } from "./http-Di8yiDMY.js";
6
6
  import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-_S2_MeLF.js";
7
- import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-D535cSz_.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DduYhpee.js";
7
+ import { n as doesActorOwnKey, r as getKeyOwner, t as verifyPortableObject } from "./portable-collection-FTPa5D91.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DpwDPt9V.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
- import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
11
+ import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, PUBLIC_COLLECTION, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
12
12
  import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
13
13
  import { FetchError, UrlError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, preloadedContexts, resolveDocumentLoaderTimeout, toCompatibleEf61Id, validatePublicUrl } from "@fedify/vocab-runtime";
14
- import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
14
+ import { getPortableGatewayCandidates, isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
15
15
  import { encodeHex } from "byte-encodings/hex";
16
16
  import jsonld from "@fedify/vocab-runtime/jsonld";
17
17
  import { uniq } from "es-toolkit";
@@ -2358,6 +2358,95 @@ async function checkServedPortableMap({ map, contexts }, options) {
2358
2358
  return UNSECURED_CORE_TYPES.has(type) ? null : "missingProof";
2359
2359
  }
2360
2360
  //#endregion
2361
+ //#region src/federation/audience.ts
2362
+ const AS_NAMESPACE = "https://www.w3.org/ns/activitystreams#";
2363
+ /**
2364
+ * The addressing properties that determine the audience of an object.
2365
+ */
2366
+ const ADDRESSING_PROPERTIES = [
2367
+ "to",
2368
+ "cc",
2369
+ "bto",
2370
+ "bcc",
2371
+ "audience"
2372
+ ];
2373
+ /**
2374
+ * The IDs that Fedify treats as the public collection. Besides the full IRI,
2375
+ * the compact forms `as:Public` and `Public` are reserved as public for
2376
+ * compatibility with documents whose compact forms survive JSON-LD expansion,
2377
+ * e.g., bare `Public`, which expands to a relative IRI.
2378
+ */
2379
+ const PUBLIC_IDS = /* @__PURE__ */ new Set([
2380
+ PUBLIC_COLLECTION.href,
2381
+ "as:Public",
2382
+ "Public"
2383
+ ]);
2384
+ /**
2385
+ * Checks whether an expanded JSON-LD node is addressed to the public
2386
+ * collection, i.e., any of its `to`, `cc`, `bto`, `bcc`, and `audience` has
2387
+ * the public collection. Only the node itself is checked, not the nodes
2388
+ * embedded in it.
2389
+ * @param node The expanded JSON-LD node.
2390
+ * @returns `true` if the node is publicly addressed.
2391
+ * @internal
2392
+ */
2393
+ function isPubliclyAddressedNode(node) {
2394
+ for (const property of ADDRESSING_PROPERTIES) {
2395
+ const values = node[AS_NAMESPACE + property];
2396
+ if (!Array.isArray(values)) continue;
2397
+ for (const value of values) if (typeof value === "object" && value != null && "@id" in value && typeof value["@id"] === "string" && PUBLIC_IDS.has(value["@id"])) return true;
2398
+ }
2399
+ return false;
2400
+ }
2401
+ function getAudienceIds(object) {
2402
+ return [
2403
+ ...object.toIds,
2404
+ ...object.ccIds,
2405
+ ...object.btoIds,
2406
+ ...object.bccIds,
2407
+ ...object.audienceIds
2408
+ ];
2409
+ }
2410
+ /**
2411
+ * Checks whether two actor IDs refer to the same actor. [FEP-ef61] portable
2412
+ * IDs are compared by their canonical forms, so an `ap:` URI, an `ap+ef61:`
2413
+ * URI, and a compatible identifier on any gateway are the same actor if they
2414
+ * have the same canonical portable ID. Other IDs are compared as they are.
2415
+ *
2416
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2417
+ */
2418
+ function isSameActorId(a, b) {
2419
+ const canonicalA = getCanonicalPortableId(a);
2420
+ const canonicalB = getCanonicalPortableId(b);
2421
+ if (canonicalA != null || canonicalB != null) return canonicalA === canonicalB;
2422
+ return a.href === b.href;
2423
+ }
2424
+ /**
2425
+ * Checks whether an actor belongs to the audience of an object: the object is
2426
+ * publicly addressed, the actor is one of its addressees, or the actor is
2427
+ * a member of one of them according to `isMember()`.
2428
+ * @param object The object whose audience is checked.
2429
+ * @param actor The actor to check, or `null` if there is no actor. No actor
2430
+ * belongs to the audience of an object that is not public.
2431
+ * @param options Options for checking the audience.
2432
+ * @returns `true` if the actor belongs to the audience of the object.
2433
+ * @internal
2434
+ */
2435
+ async function isInAudience(object, actor, options = {}) {
2436
+ const addressees = getAudienceIds(object);
2437
+ if (addressees.some((id) => PUBLIC_IDS.has(id.href))) return true;
2438
+ const actorId = actor?.id;
2439
+ if (actor == null || actorId == null) return false;
2440
+ const others = [];
2441
+ for (const addressee of addressees) {
2442
+ if (isSameActorId(addressee, actorId)) return true;
2443
+ others.push(addressee);
2444
+ }
2445
+ if (options.isMember == null) return false;
2446
+ for (const addressee of others) if (await options.isMember(addressee, actor)) return true;
2447
+ return false;
2448
+ }
2449
+ //#endregion
2361
2450
  //#region src/federation/inbox.ts
2362
2451
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
2363
2452
  const logger = getLogger([
@@ -2980,31 +3069,177 @@ function parseHashlinkGatewayRequest(url) {
2980
3069
  })
2981
3070
  };
2982
3071
  }
3072
+ /**
3073
+ * Resolves a portable inbox, i.e., an `ap:` or `ap+ef61:` URI, to
3074
+ * the compatible identifiers of the inbox on the recipient's gateways.
3075
+ *
3076
+ * The gateways are taken from the recipient's `gateways`, or, if it has no
3077
+ * valid gateway, from the `@gateway` location hints of the inbox URI. Both
3078
+ * come from the recipient object, so they are as trustworthy as its inbox.
3079
+ * Gateways whose origins are in `excludeBaseUris` are skipped.
3080
+ * @param recipient The recipient that owns the inbox.
3081
+ * @param inbox The portable inbox.
3082
+ * @param excludeBaseUris The base URIs whose origins are not delivered to.
3083
+ * @returns The delivery target, or `null` if the inbox is malformed, or no
3084
+ * gateway is left to deliver through.
3085
+ */
3086
+ function resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris) {
3087
+ const logger = getLogger([
3088
+ "fedify",
3089
+ "federation",
3090
+ "outbox"
3091
+ ]);
3092
+ let portableInbox;
3093
+ try {
3094
+ portableInbox = canonicalizePortableUri(inbox.href);
3095
+ } catch (error) {
3096
+ if (!(error instanceof TypeError)) throw error;
3097
+ logger.warn("Skipping the recipient {recipient}, as its portable inbox {inbox} is malformed.", {
3098
+ recipient: recipient.id?.href,
3099
+ inbox: inbox.href,
3100
+ error
3101
+ });
3102
+ return null;
3103
+ }
3104
+ let gateways = (recipient.gateways ?? []).filter(isGatewayUrl);
3105
+ if (gateways.length < 1) gateways = getPortableGatewayCandidates(inbox);
3106
+ if (gateways.length < 1) {
3107
+ logger.warn("Skipping the recipient {recipient}, as it has no gateway to deliver to its portable inbox {inbox} through.", {
3108
+ recipient: recipient.id?.href,
3109
+ inbox: portableInbox
3110
+ });
3111
+ return null;
3112
+ }
3113
+ const excludedOrigins = new Set(excludeBaseUris?.map((u) => u.origin));
3114
+ const seen = /* @__PURE__ */ new Set();
3115
+ const inboxes = [];
3116
+ for (const gateway of gateways) {
3117
+ if (inboxes.length >= 5) break;
3118
+ if (seen.has(gateway.origin)) continue;
3119
+ seen.add(gateway.origin);
3120
+ if (excludedOrigins.has(gateway.origin)) continue;
3121
+ try {
3122
+ inboxes.push(toCompatibleEf61Id(inbox, gateway.origin));
3123
+ } catch (error) {
3124
+ if (!(error instanceof TypeError)) throw error;
3125
+ logger.warn("Cannot deliver to the portable inbox {inbox} through the gateway {gateway}, as the inbox cannot be represented as its compatible identifier.", {
3126
+ inbox: portableInbox,
3127
+ gateway: gateway.origin,
3128
+ error
3129
+ });
3130
+ }
3131
+ }
3132
+ if (inboxes.length < 1) {
3133
+ logger.debug("Skipping the recipient {recipient}, as no gateway is left to deliver to its portable inbox {inbox} through.", {
3134
+ recipient: recipient.id?.href,
3135
+ inbox: portableInbox
3136
+ });
3137
+ return null;
3138
+ }
3139
+ return {
3140
+ portableInbox,
3141
+ inboxes
3142
+ };
3143
+ }
3144
+ /**
3145
+ * Merges two ordered lists of compatible inbox URLs, keeping the first URL
3146
+ * for each gateway origin and at most {@link MAX_DELIVERY_GATEWAYS} URLs.
3147
+ * @param a The preferred list.
3148
+ * @param b The other list.
3149
+ * @returns The merged list.
3150
+ */
3151
+ function mergeGatewayInboxes(a, b) {
3152
+ const seen = /* @__PURE__ */ new Set();
3153
+ const merged = [];
3154
+ for (const inbox of [...a, ...b]) {
3155
+ if (merged.length >= 5) break;
3156
+ const { origin } = new URL(inbox);
3157
+ if (seen.has(origin)) continue;
3158
+ seen.add(origin);
3159
+ merged.push(inbox);
3160
+ }
3161
+ return merged;
3162
+ }
3163
+ /**
3164
+ * Gets the part of a delivery's ordering key that tells its destination
3165
+ * apart: the DID of a portable inbox, so that deliveries to it stay in order
3166
+ * whichever gateway they go through, or the origin of any other inbox.
3167
+ * @param inbox The inbox URL.
3168
+ * @param portableInbox The canonical portable inbox, if the inbox is
3169
+ * the compatible identifier of one.
3170
+ * @returns The destination part of the ordering key.
3171
+ */
3172
+ function getOrderingDestination(inbox, portableInbox) {
3173
+ return portableInbox == null ? new URL(inbox).origin : getFe34Origin(portableInbox);
3174
+ }
2983
3175
  //#endregion
2984
3176
  //#region src/federation/send.ts
2985
3177
  /**
2986
3178
  * Extracts the inbox URLs from recipients.
3179
+ *
3180
+ * An [FEP-ef61] portable inbox, i.e., an `ap:` or `ap+ef61:` URI, is resolved
3181
+ * to its compatible identifiers on the recipient's gateways, or, if the
3182
+ * recipient has no valid gateway, on the gateways in the `@gateway` location
3183
+ * hints of the inbox URI. The inbox is keyed by the compatible identifier on
3184
+ * the first gateway, and the others are listed in
3185
+ * {@link ExtractedInbox.gatewayInboxes}. A recipient whose portable inbox
3186
+ * has no gateway to deliver through is skipped. The shared inbox of
3187
+ * a recipient with a portable inbox is never preferred, since only a portable
3188
+ * inbox is synchronized between the gateways.
3189
+ *
3190
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2987
3191
  * @param parameters The parameters to extract the inboxes.
2988
3192
  * See also {@link ExtractInboxesParameters}.
2989
3193
  * @returns The inboxes as a map of inbox URL to actor URIs.
2990
3194
  */
2991
3195
  function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2992
3196
  const inboxes = {};
3197
+ const portableKeys = /* @__PURE__ */ new Map();
3198
+ const compatibleKeys = /* @__PURE__ */ new Map();
2993
3199
  for (const recipient of recipients) {
3200
+ if (recipient.id == null) continue;
2994
3201
  let inbox;
2995
3202
  let sharedInbox = false;
2996
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
3203
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null && (recipient.inboxId == null || !isPortableUri(recipient.inboxId))) {
2997
3204
  inbox = recipient.endpoints.sharedInbox;
2998
3205
  sharedInbox = true;
2999
3206
  } else inbox = recipient.inboxId;
3000
- if (inbox != null && recipient.id != null) {
3001
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
3002
- inboxes[inbox.href] ??= {
3207
+ if (inbox == null) continue;
3208
+ if (isPortableUri(inbox)) {
3209
+ const target = resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris);
3210
+ if (target == null) continue;
3211
+ const gatewayInboxes = target.inboxes.map((u) => u.href);
3212
+ const existingKeys = /* @__PURE__ */ new Set();
3213
+ const portableKey = portableKeys.get(target.portableInbox);
3214
+ if (portableKey != null) existingKeys.add(portableKey);
3215
+ for (const i of gatewayInboxes) {
3216
+ const k = compatibleKeys.get(i) ?? i;
3217
+ if (Object.hasOwn(inboxes, k)) existingKeys.add(k);
3218
+ }
3219
+ const [key = gatewayInboxes[0], ...otherKeys] = existingKeys;
3220
+ portableKeys.set(target.portableInbox, key);
3221
+ const entry = inboxes[key] ??= {
3003
3222
  actorIds: /* @__PURE__ */ new Set(),
3004
3223
  sharedInbox
3005
3224
  };
3006
- inboxes[inbox.href].actorIds.add(recipient.id.href);
3225
+ for (const k of otherKeys) {
3226
+ for (const actorId of inboxes[k].actorIds) entry.actorIds.add(actorId);
3227
+ delete inboxes[k];
3228
+ }
3229
+ entry.sharedInbox = false;
3230
+ entry.portableInbox = target.portableInbox;
3231
+ entry.gatewayInboxes = mergeGatewayInboxes(entry.gatewayInboxes ?? [key], gatewayInboxes);
3232
+ for (const i of entry.gatewayInboxes) compatibleKeys.set(i, key);
3233
+ entry.actorIds.add(recipient.id.href);
3234
+ continue;
3007
3235
  }
3236
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
3237
+ const key = compatibleKeys.get(inbox.href) ?? inbox.href;
3238
+ inboxes[key] ??= {
3239
+ actorIds: /* @__PURE__ */ new Set(),
3240
+ sharedInbox
3241
+ };
3242
+ inboxes[key].actorIds.add(recipient.id.href);
3008
3243
  }
3009
3244
  return inboxes;
3010
3245
  }
@@ -3039,6 +3274,38 @@ function sendActivity(options) {
3039
3274
  }
3040
3275
  });
3041
3276
  }
3277
+ /**
3278
+ * Sends an activity to the first of the given inboxes that accepts it, i.e.,
3279
+ * the compatible identifiers of a portable inbox on its gateways, trying them
3280
+ * one after another.
3281
+ * @param parameters The parameters, except for the inbox.
3282
+ * @param inboxes The inboxes to try, in order. Must not be empty.
3283
+ * @throws {Error} The error of the last inbox if no inbox accepts it.
3284
+ */
3285
+ async function sendActivityThroughGateways(parameters, inboxes) {
3286
+ const logger = getLogger([
3287
+ "fedify",
3288
+ "federation",
3289
+ "outbox"
3290
+ ]);
3291
+ let lastError;
3292
+ for (const [i, inbox] of inboxes.entries()) try {
3293
+ await sendActivity({
3294
+ ...parameters,
3295
+ inbox
3296
+ });
3297
+ return;
3298
+ } catch (error) {
3299
+ lastError = error;
3300
+ if (i < inboxes.length - 1) logger.warn("Failed to send activity {activityId} to {inbox}; trying the next gateway {next}:\n{error}", {
3301
+ activityId: parameters.activityId,
3302
+ inbox: inbox.href,
3303
+ next: inboxes[i + 1].href,
3304
+ error
3305
+ });
3306
+ }
3307
+ throw lastError;
3308
+ }
3042
3309
  const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
3043
3310
  function getActivityActorId(activity) {
3044
3311
  if (!isRecord(activity)) return void 0;
@@ -3891,6 +4158,15 @@ async function handleObject(request, { values, context, objectDispatcher, author
3891
4158
  * only if its ID canonically matches the requested portable ID and it
3892
4159
  * satisfies the FEP-ef61 proof policy.
3893
4160
  *
4161
+ * FEP-ef61 forbids serving a non-public object to anyone but its intended
4162
+ * audience. If the dispatcher has an authorization predicate, it decides who
4163
+ * may retrieve the object, and the response is marked with
4164
+ * `Cache-Control: private`, as it depends on the requester. Otherwise, only
4165
+ * a publicly addressed object is served, and any other object is responded
4166
+ * to with `404 Not Found`, as if this server did not store it. Actors, and
4167
+ * anything the actor dispatcher returns, are exempt from the latter, as
4168
+ * actors have no audience.
4169
+ *
3894
4170
  * A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
3895
4171
  * Proof made with a key of the DID in its ID. An unsigned tombstone cannot
3896
4172
  * be served as a portable object, so the response is the same as for
@@ -3900,7 +4176,7 @@ async function handleObject(request, { values, context, objectDispatcher, author
3900
4176
  * @param parameters The parameters for handling the portable object.
3901
4177
  * @returns A promise that resolves to an HTTP response.
3902
4178
  */
3903
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
4179
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, kind, onNotFound, onUnauthorized }) {
3904
4180
  const logger = getLogger([
3905
4181
  "fedify",
3906
4182
  "federation",
@@ -3919,14 +4195,32 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3919
4195
  if (authorizePredicate != null) {
3920
4196
  if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3921
4197
  }
4198
+ const contextLoader = getNormalizationContextLoader(context.contextLoader);
3922
4199
  let jsonLd;
3923
- let servedId;
3924
- let result;
4200
+ let root;
3925
4201
  try {
3926
4202
  jsonLd = await object.toJsonLd(context);
3927
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
4203
+ root = await expandJsonLdRoot(jsonLd, contextLoader);
4204
+ } catch (error) {
4205
+ logger.error("Failed to serialize the portable object {portableId}:\n{error}", {
4206
+ portableId: canonicalId,
4207
+ error
4208
+ });
4209
+ return portableObjectInternalServerError(request);
4210
+ }
4211
+ const servedId = root?.["@id"];
4212
+ if (root == null || typeof servedId !== "string" || !isRequestedPortableId(servedId, canonicalId)) {
4213
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
4214
+ return await onNotFound(request);
4215
+ }
4216
+ if (authorizePredicate == null && kind === "object" && !isActor(object) && !isPubliclyAddressedNode(root)) {
4217
+ 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 });
4218
+ return await onNotFound(request);
4219
+ }
4220
+ let result;
4221
+ try {
3928
4222
  result = await verifyPortableObjectProof(jsonLd, {
3929
- contextLoader: context.contextLoader,
4223
+ contextLoader,
3930
4224
  documentLoader: context.documentLoader,
3931
4225
  tracerProvider: context.tracerProvider,
3932
4226
  meterProvider: context.meterProvider
@@ -3938,10 +4232,6 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3938
4232
  });
3939
4233
  return portableObjectInternalServerError(request);
3940
4234
  }
3941
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
3942
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
3943
- return await onNotFound(request);
3944
- }
3945
4235
  const tombstone = object instanceof Tombstone;
3946
4236
  if (!result.verified) {
3947
4237
  if (tombstone && result.reason.type === "missingProof") {
@@ -3960,25 +4250,26 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3960
4250
  return portableObjectInternalServerError(request);
3961
4251
  }
3962
4252
  }
4253
+ const headers = new Headers({
4254
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
4255
+ Vary: "Accept"
4256
+ });
4257
+ if (authorizePredicate != null) headers.set("Cache-Control", "private");
3963
4258
  return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
3964
4259
  status: tombstone ? 410 : 200,
3965
- headers: {
3966
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3967
- Vary: "Accept"
3968
- }
4260
+ headers
3969
4261
  });
3970
4262
  }
3971
4263
  function isRequestedPortableObject(object, canonicalId) {
3972
4264
  return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
3973
4265
  }
3974
- async function getJsonLdRootId(jsonLd, contextLoader) {
4266
+ async function expandJsonLdRoot(jsonLd, contextLoader) {
3975
4267
  const expanded = await jsonld.expand(jsonLd, {
3976
- documentLoader: getNormalizationContextLoader(contextLoader),
4268
+ documentLoader: contextLoader,
3977
4269
  keepFreeFloatingNodes: true
3978
4270
  });
3979
4271
  if (expanded.length !== 1) return null;
3980
- const id = expanded[0]["@id"];
3981
- return typeof id === "string" ? id : null;
4272
+ return expanded[0];
3982
4273
  }
3983
4274
  function isRequestedPortableId(id, canonicalId) {
3984
4275
  if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
@@ -7133,6 +7424,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
7133
7424
  function maxDelay(first, second) {
7134
7425
  return Temporal.Duration.compare(first, second) >= 0 ? first : second;
7135
7426
  }
7427
+ /**
7428
+ * Gets the fields of a queued message that describe the FEP-ef61 portable
7429
+ * inbox an inbox stands for, if any, omitting the absent ones.
7430
+ */
7431
+ function getPortableMessageFields(portableInbox, gatewayInboxes) {
7432
+ return {
7433
+ ...portableInbox == null ? {} : { portableInbox },
7434
+ ...gatewayInboxes == null ? {} : { gatewayInboxes: [...gatewayInboxes] }
7435
+ };
7436
+ }
7437
+ /**
7438
+ * Sends an activity to an inbox, or, if the inbox is the compatible
7439
+ * identifier of an FEP-ef61 portable inbox on more than one gateway, through
7440
+ * the first gateway that accepts it.
7441
+ */
7442
+ function sendActivityToInbox(inbox, gatewayInboxes, parameters) {
7443
+ if (gatewayInboxes == null || gatewayInboxes.length < 2) return sendActivity({
7444
+ ...parameters,
7445
+ inbox: new URL(inbox)
7446
+ });
7447
+ return sendActivityThroughGateways(parameters, gatewayInboxes.map((i) => new URL(i)));
7448
+ }
7449
+ function parseInstant(value) {
7450
+ if (value == null) return void 0;
7451
+ try {
7452
+ return Temporal.Instant.from(value);
7453
+ } catch (error) {
7454
+ if (error instanceof RangeError) return void 0;
7455
+ throw error;
7456
+ }
7457
+ }
7136
7458
  function isTransportDeliveryError(error) {
7137
7459
  return error instanceof FetchError || isAbortError(error);
7138
7460
  }
@@ -7713,6 +8035,20 @@ var FederationImpl = class extends FederationBuilderImpl {
7713
8035
  }
7714
8036
  recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
7715
8037
  };
8038
+ const { gatewayInboxes } = message;
8039
+ if (gatewayInboxes != null && gatewayInboxes.length > 1) {
8040
+ await this.#listenGatewayOutboxMessage(_, {
8041
+ ...message,
8042
+ gatewayInboxes
8043
+ }, span, {
8044
+ keys,
8045
+ logData,
8046
+ getActorIds,
8047
+ parseActivity,
8048
+ dropHeldOutboxMessage
8049
+ });
8050
+ return;
8051
+ }
7716
8052
  try {
7717
8053
  const inbox = new URL(message.inbox);
7718
8054
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
@@ -7935,6 +8271,308 @@ var FederationImpl = class extends FederationBuilderImpl {
7935
8271
  }
7936
8272
  logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
7937
8273
  }
8274
+ /**
8275
+ * Delivers a queued activity to an FEP-ef61 portable inbox through the first
8276
+ * of its gateways that accepts it, trying them one after another in a single
8277
+ * round, so that the round counts as one attempt for the retry policy, keeps
8278
+ * the message's place in its ordering key, and is replayed as a whole by
8279
+ * queues that retry natively.
8280
+ *
8281
+ * A gateway that fails with a permanent failure status is not tried again.
8282
+ * A gateway whose circuit is open, or which asked to be retried later with
8283
+ * `Retry-After`, is skipped in this round. If no gateway accepts the
8284
+ * activity, the message is held while every remaining gateway is held, and
8285
+ * otherwise retried according to the retry policy.
8286
+ */
8287
+ async #listenGatewayOutboxMessage(contextData, message, span, helpers) {
8288
+ const logger = getLogger([
8289
+ "fedify",
8290
+ "federation",
8291
+ "outbox"
8292
+ ]);
8293
+ const circuitLogger = getLogger([
8294
+ "fedify",
8295
+ "federation",
8296
+ "circuit"
8297
+ ]);
8298
+ const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
8299
+ const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
8300
+ const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
8301
+ const survivors = [];
8302
+ let lastError;
8303
+ let lastPermanent;
8304
+ let activity;
8305
+ const getActivity = async () => activity ??= await parseActivity();
8306
+ const drop = async (circuit, remoteHost, inbox, heldSince) => {
8307
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
8308
+ };
8309
+ const heldSince = parseInstant(message.circuitHeldSince);
8310
+ if (circuit != null && heldSince != null && Temporal.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal.Now.instant()) <= 0) {
8311
+ const inbox = new URL(message.gatewayInboxes[0]);
8312
+ await drop(circuit, getRemoteHost(inbox), inbox, heldSince);
8313
+ return;
8314
+ }
8315
+ for (const inboxHref of message.gatewayInboxes) {
8316
+ const inbox = new URL(inboxHref);
8317
+ const remoteHost = getRemoteHost(inbox);
8318
+ const now = Temporal.Now.instant();
8319
+ const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
8320
+ if (notBefore != null && Temporal.Instant.compare(notBefore, now) > 0) {
8321
+ survivors.push({
8322
+ inbox: inboxHref,
8323
+ holdDelay: now.until(notBefore),
8324
+ notBefore
8325
+ });
8326
+ continue;
8327
+ }
8328
+ let decision;
8329
+ if (circuit != null) try {
8330
+ decision = await circuit.beforeSend(remoteHost, message);
8331
+ } catch (circuitError) {
8332
+ circuitLogger.error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
8333
+ ...logData,
8334
+ remoteHost,
8335
+ error: circuitError
8336
+ });
8337
+ }
8338
+ if (decision != null && circuit != null) {
8339
+ if (decision.type === "drop") {
8340
+ await drop(circuit, remoteHost, inbox, decision.heldSince);
8341
+ return;
8342
+ } else if (decision.type === "hold") {
8343
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
8344
+ survivors.push({
8345
+ inbox: inboxHref,
8346
+ holdDelay: decision.delay,
8347
+ circuitHeld: true
8348
+ });
8349
+ continue;
8350
+ } else if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
8351
+ }
8352
+ try {
8353
+ await sendActivity({
8354
+ allowPrivateAddress: this.allowPrivateAddress,
8355
+ keys,
8356
+ activity: message.activity,
8357
+ activityId: message.activityId,
8358
+ activityType: message.activityType,
8359
+ inbox,
8360
+ sharedInbox: message.sharedInbox,
8361
+ headers: new Headers(message.headers),
8362
+ specDeterminer,
8363
+ meterProvider: this.meterProvider,
8364
+ tracerProvider: this.tracerProvider
8365
+ });
8366
+ } catch (error) {
8367
+ lastError = error;
8368
+ const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
8369
+ let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
8370
+ let retryAt;
8371
+ if (retryAfter != null) try {
8372
+ retryAt = Temporal.Now.instant().add(retryAfter);
8373
+ } catch (e) {
8374
+ if (!(e instanceof RangeError)) throw e;
8375
+ retryAfter = void 0;
8376
+ }
8377
+ let postDecision;
8378
+ if (circuit != null) try {
8379
+ if (error instanceof SendActivityError) {
8380
+ const { statusCode } = error;
8381
+ const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await circuit.recordReachableFailure(remoteHost) : statusCode >= 500 ? await circuit.recordFailure(remoteHost) : void 0;
8382
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8383
+ } else if (isTransportDeliveryError(error)) {
8384
+ const stateChange = await circuit.recordFailure(remoteHost);
8385
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8386
+ }
8387
+ if (!isPermanentFailure) postDecision = await circuit.beforeSend(remoteHost, message);
8388
+ } catch (circuitError) {
8389
+ circuitLogger.error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
8390
+ ...logData,
8391
+ remoteHost,
8392
+ error: circuitError
8393
+ });
8394
+ }
8395
+ span.addEvent("activitypub.delivery.failed", {
8396
+ "activitypub.remote.host": remoteHost,
8397
+ "activitypub.delivery.attempt": message.attempt,
8398
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
8399
+ ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
8400
+ });
8401
+ try {
8402
+ await this.onOutboxError?.(error, await getActivity());
8403
+ } catch (handlerError) {
8404
+ logger.error("An unexpected error occurred in onError handler:\n{error}", {
8405
+ ...logData,
8406
+ inbox: inboxHref,
8407
+ error: handlerError
8408
+ });
8409
+ }
8410
+ if (isPermanentFailure) {
8411
+ logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
8412
+ ...logData,
8413
+ inbox: inboxHref,
8414
+ status: error.statusCode
8415
+ });
8416
+ lastPermanent = {
8417
+ error,
8418
+ inbox
8419
+ };
8420
+ continue;
8421
+ }
8422
+ if (postDecision?.type === "drop" && circuit != null) {
8423
+ await drop(circuit, remoteHost, inbox, postDecision.heldSince);
8424
+ return;
8425
+ } else if (postDecision?.type === "hold") {
8426
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, postDecision.state);
8427
+ survivors.push({
8428
+ inbox: inboxHref,
8429
+ holdDelay: retryAfter == null ? postDecision.delay : maxDelay(postDecision.delay, retryAfter),
8430
+ circuitHeld: true,
8431
+ postFailure: true,
8432
+ notBefore: retryAt,
8433
+ retryAfter
8434
+ });
8435
+ } else survivors.push({
8436
+ inbox: inboxHref,
8437
+ notBefore: retryAt,
8438
+ retryAfter
8439
+ });
8440
+ logger.warn("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); trying the next gateway if any:\n{error}", {
8441
+ ...logData,
8442
+ inbox: inboxHref,
8443
+ error
8444
+ });
8445
+ continue;
8446
+ }
8447
+ if (circuit != null) try {
8448
+ const stateChange = await circuit.recordSuccess(remoteHost);
8449
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8450
+ } catch (error) {
8451
+ circuitLogger.error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
8452
+ ...logData,
8453
+ remoteHost,
8454
+ error
8455
+ });
8456
+ }
8457
+ logger.info("Successfully sent activity {activityId} to {inbox}.", {
8458
+ ...logData,
8459
+ inbox: inboxHref
8460
+ });
8461
+ return;
8462
+ }
8463
+ if (lastError != null) span.setStatus({
8464
+ code: SpanStatusCode.ERROR,
8465
+ message: String(lastError)
8466
+ });
8467
+ if (survivors.length < 1) {
8468
+ const { error, inbox } = lastPermanent;
8469
+ this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
8470
+ logger.warn("Permanent delivery failure for activity {activityId} to every gateway of {portableInbox}; not retrying.", {
8471
+ ...logData,
8472
+ portableInbox: message.portableInbox
8473
+ });
8474
+ if (this.outboxPermanentFailureHandler != null) {
8475
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
8476
+ try {
8477
+ await this.outboxPermanentFailureHandler(ctx, {
8478
+ reason: "http",
8479
+ inbox,
8480
+ activity: await getActivity(),
8481
+ error,
8482
+ statusCode: error.statusCode,
8483
+ actorIds: getActorIds()
8484
+ });
8485
+ } catch (handlerError) {
8486
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
8487
+ ...logData,
8488
+ error: handlerError
8489
+ });
8490
+ }
8491
+ }
8492
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
8493
+ return;
8494
+ }
8495
+ const now = Temporal.Now.instant();
8496
+ const gatewayNotBefore = {};
8497
+ for (const survivor of survivors) if (survivor.notBefore != null && Temporal.Instant.compare(survivor.notBefore, now) > 0) gatewayNotBefore[survivor.inbox] = survivor.notBefore.toString();
8498
+ const { circuitHeld: _circuitHeld, gatewayNotBefore: _gatewayNotBefore, ...rest } = message;
8499
+ const nextMessage = {
8500
+ ...rest,
8501
+ inbox: survivors[0].inbox,
8502
+ gatewayInboxes: survivors.map((s) => s.inbox),
8503
+ ...globalThis.Object.keys(gatewayNotBefore).length < 1 ? {} : { gatewayNotBefore }
8504
+ };
8505
+ const { outboxQueue } = this;
8506
+ const policyDelay = this.outboxRetryPolicy({
8507
+ elapsedTime: Temporal.Instant.from(message.started).until(now),
8508
+ attempts: message.attempt
8509
+ });
8510
+ if (outboxQueue != null && survivors.every((s) => s.holdDelay != null) && (policyDelay != null || !survivors.some((s) => s.postFailure))) {
8511
+ let delay = survivors.map((s) => s.holdDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
8512
+ let heldMessage = nextMessage;
8513
+ if (survivors.some((s) => s.circuitHeld)) {
8514
+ const since = heldSince ?? now;
8515
+ if (circuit != null) delay = circuit.capHeldDelay(since, delay);
8516
+ heldMessage = {
8517
+ ...nextMessage,
8518
+ circuitHeld: true,
8519
+ circuitHeldSince: since.toString()
8520
+ };
8521
+ }
8522
+ logger.warn("Failed to send activity {activityId} to {portableInbox}; holding it until one of its gateways can be tried again.", {
8523
+ ...logData,
8524
+ portableInbox: message.portableInbox
8525
+ });
8526
+ await outboxQueue.enqueue(heldMessage, {
8527
+ delay: clampNegativeDelay(delay),
8528
+ orderingKey: message.orderingKey
8529
+ });
8530
+ this.metrics.recordQueueTaskEnqueued({
8531
+ role: "outbox",
8532
+ queue: outboxQueue,
8533
+ activityType: heldMessage.activityType
8534
+ }, heldMessage.attempt);
8535
+ return;
8536
+ }
8537
+ const hasRetryAfter = survivors.some((s) => s.retryAfter != null);
8538
+ if (outboxQueue?.nativeRetrial && !hasRetryAfter && survivors.length === message.gatewayInboxes.length && globalThis.Object.keys(gatewayNotBefore).length < 1) {
8539
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox}; backend will handle retry:\n{error}", {
8540
+ ...logData,
8541
+ portableInbox: message.portableInbox,
8542
+ error: lastError
8543
+ });
8544
+ throw lastError;
8545
+ }
8546
+ if (policyDelay == null || outboxQueue == null) {
8547
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
8548
+ ...logData,
8549
+ portableInbox: message.portableInbox,
8550
+ error: lastError
8551
+ });
8552
+ recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
8553
+ return;
8554
+ }
8555
+ const delay = survivors.map((s) => s.holdDelay ?? s.retryAfter ?? policyDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
8556
+ const retryMessage = {
8557
+ ...nextMessage,
8558
+ attempt: message.attempt + 1
8559
+ };
8560
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} (attempt #{attempt}); retry...:\n{error}", {
8561
+ ...logData,
8562
+ portableInbox: message.portableInbox,
8563
+ error: lastError
8564
+ });
8565
+ await outboxQueue.enqueue(retryMessage, {
8566
+ delay: clampNegativeDelay(delay),
8567
+ orderingKey: message.orderingKey
8568
+ });
8569
+ this.metrics.recordQueueTaskEnqueued({
8570
+ role: "outbox",
8571
+ queue: outboxQueue,
8572
+ activityType: retryMessage.activityType
8573
+ }, retryMessage.attempt);
8574
+ recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
8575
+ }
7938
8576
  async #listenInboxMessage(ctxData, message, span, onActivityType) {
7939
8577
  const logger = getLogger([
7940
8578
  "fedify",
@@ -8364,13 +9002,12 @@ var FederationImpl = class extends FederationBuilderImpl {
8364
9002
  activity: jsonLd
8365
9003
  });
8366
9004
  const promises = [];
8367
- for (const inbox in inboxes) promises.push(sendActivity({
9005
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
8368
9006
  allowPrivateAddress: this.allowPrivateAddress,
8369
9007
  keys,
8370
9008
  activity: jsonLd,
8371
9009
  activityId: activity.id?.href,
8372
9010
  activityType: getTypeId(activity).href,
8373
- inbox: new URL(inbox),
8374
9011
  sharedInbox: inboxes[inbox].sharedInbox,
8375
9012
  headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
8376
9013
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
@@ -8397,8 +9034,8 @@ var FederationImpl = class extends FederationBuilderImpl {
8397
9034
  propagation.inject(context.active(), carrier);
8398
9035
  const messages = [];
8399
9036
  for (const inbox in inboxes) {
8400
- const inboxOrigin = new URL(inbox).origin;
8401
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${inboxOrigin}`;
9037
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
9038
+ const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
8402
9039
  const message = {
8403
9040
  type: "outbox",
8404
9041
  id: crypto.randomUUID(),
@@ -8414,6 +9051,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8414
9051
  attempt: 0,
8415
9052
  headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
8416
9053
  orderingKey: messageOrderingKey,
9054
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
8417
9055
  traceContext: carrier
8418
9056
  };
8419
9057
  messages.push({
@@ -8863,6 +9501,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8863
9501
  objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
8864
9502
  authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
8865
9503
  canonicalId: portable.canonicalId,
9504
+ kind: "actor",
8866
9505
  onUnauthorized,
8867
9506
  onNotFound
8868
9507
  });
@@ -8883,6 +9522,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8883
9522
  objectDispatcher: callbacks?.dispatcher,
8884
9523
  authorizePredicate: callbacks?.authorizePredicate,
8885
9524
  canonicalId: portable.canonicalId,
9525
+ kind: "object",
8886
9526
  onUnauthorized,
8887
9527
  onNotFound
8888
9528
  });
@@ -9879,9 +10519,10 @@ var ContextImpl = class ContextImpl {
9879
10519
  id: crypto.randomUUID(),
9880
10520
  baseUrl: this.origin,
9881
10521
  keys: keyJwkPairs,
9882
- inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
10522
+ inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
9883
10523
  actorIds: [...actorIds],
9884
- sharedInbox
10524
+ sharedInbox,
10525
+ ...getPortableMessageFields(portableInbox, gatewayInboxes)
9885
10526
  }])),
9886
10527
  activity: activityJsonLd,
9887
10528
  activityId: activity.id?.href,
@@ -10178,6 +10819,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
10178
10819
  throw error;
10179
10820
  }
10180
10821
  }
10822
+ async isSignedByAudience(object, options = {}) {
10823
+ return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
10824
+ }
10181
10825
  };
10182
10826
  function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
10183
10827
  return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
@@ -10265,13 +10909,12 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10265
10909
  if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
10266
10910
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
10267
10911
  const promises = [];
10268
- for (const inbox in inboxes) promises.push(sendActivity({
10912
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
10269
10913
  allowPrivateAddress: ctx.federation.allowPrivateAddress,
10270
10914
  keys,
10271
10915
  activity: ctx.activity,
10272
10916
  activityId: ctx.activityId,
10273
10917
  activityType: ctx.activityType,
10274
- inbox: new URL(inbox),
10275
10918
  sharedInbox: inboxes[inbox].sharedInbox,
10276
10919
  meterProvider: ctx.meterProvider,
10277
10920
  tracerProvider: ctx.tracerProvider,
@@ -10299,7 +10942,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10299
10942
  const started = (/* @__PURE__ */ new Date()).toISOString();
10300
10943
  const messages = [];
10301
10944
  for (const inbox in inboxes) {
10302
- const inboxUrl = new URL(inbox);
10945
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
10303
10946
  const message = {
10304
10947
  type: "outbox",
10305
10948
  id: crypto.randomUUID(),
@@ -10314,7 +10957,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10314
10957
  started,
10315
10958
  attempt: 0,
10316
10959
  headers: {},
10317
- orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
10960
+ orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
10961
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
10318
10962
  traceContext: carrier
10319
10963
  };
10320
10964
  messages.push({