@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
@@ -1,10 +1,10 @@
1
1
  const { URLPattern } = require("urlpattern-polyfill");
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-Dmm3t92W.cjs");
3
3
  const require_transformers = require("./transformers-BalCrSsr.cjs");
4
- const require_http = require("./http-BEivI0TS.cjs");
4
+ const require_http = require("./http-ByBlnY3D.cjs");
5
5
  const require_types = require("./types-poIao40f.cjs");
6
- const require_portable_collection = require("./portable-collection-BFSShzwh.cjs");
7
- const require_kv_cache = require("./kv-cache-1hafSoqH.cjs");
6
+ const require_portable_collection = require("./portable-collection-CXcAlwei.cjs");
7
+ const require_kv_cache = require("./kv-cache-BDwUzxBJ.cjs");
8
8
  let _logtape_logtape = require("@logtape/logtape");
9
9
  let _fedify_uri_template = require("@fedify/uri-template");
10
10
  let _fedify_vocab = require("@fedify/vocab");
@@ -2359,6 +2359,95 @@ async function checkServedPortableMap({ map, contexts }, options) {
2359
2359
  return UNSECURED_CORE_TYPES.has(type) ? null : "missingProof";
2360
2360
  }
2361
2361
  //#endregion
2362
+ //#region src/federation/audience.ts
2363
+ const AS_NAMESPACE = "https://www.w3.org/ns/activitystreams#";
2364
+ /**
2365
+ * The addressing properties that determine the audience of an object.
2366
+ */
2367
+ const ADDRESSING_PROPERTIES = [
2368
+ "to",
2369
+ "cc",
2370
+ "bto",
2371
+ "bcc",
2372
+ "audience"
2373
+ ];
2374
+ /**
2375
+ * The IDs that Fedify treats as the public collection. Besides the full IRI,
2376
+ * the compact forms `as:Public` and `Public` are reserved as public for
2377
+ * compatibility with documents whose compact forms survive JSON-LD expansion,
2378
+ * e.g., bare `Public`, which expands to a relative IRI.
2379
+ */
2380
+ const PUBLIC_IDS = /* @__PURE__ */ new Set([
2381
+ _fedify_vocab.PUBLIC_COLLECTION.href,
2382
+ "as:Public",
2383
+ "Public"
2384
+ ]);
2385
+ /**
2386
+ * Checks whether an expanded JSON-LD node is addressed to the public
2387
+ * collection, i.e., any of its `to`, `cc`, `bto`, `bcc`, and `audience` has
2388
+ * the public collection. Only the node itself is checked, not the nodes
2389
+ * embedded in it.
2390
+ * @param node The expanded JSON-LD node.
2391
+ * @returns `true` if the node is publicly addressed.
2392
+ * @internal
2393
+ */
2394
+ function isPubliclyAddressedNode(node) {
2395
+ for (const property of ADDRESSING_PROPERTIES) {
2396
+ const values = node[AS_NAMESPACE + property];
2397
+ if (!Array.isArray(values)) continue;
2398
+ 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;
2399
+ }
2400
+ return false;
2401
+ }
2402
+ function getAudienceIds(object) {
2403
+ return [
2404
+ ...object.toIds,
2405
+ ...object.ccIds,
2406
+ ...object.btoIds,
2407
+ ...object.bccIds,
2408
+ ...object.audienceIds
2409
+ ];
2410
+ }
2411
+ /**
2412
+ * Checks whether two actor IDs refer to the same actor. [FEP-ef61] portable
2413
+ * IDs are compared by their canonical forms, so an `ap:` URI, an `ap+ef61:`
2414
+ * URI, and a compatible identifier on any gateway are the same actor if they
2415
+ * have the same canonical portable ID. Other IDs are compared as they are.
2416
+ *
2417
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2418
+ */
2419
+ function isSameActorId(a, b) {
2420
+ const canonicalA = require_http.getCanonicalPortableId(a);
2421
+ const canonicalB = require_http.getCanonicalPortableId(b);
2422
+ if (canonicalA != null || canonicalB != null) return canonicalA === canonicalB;
2423
+ return a.href === b.href;
2424
+ }
2425
+ /**
2426
+ * Checks whether an actor belongs to the audience of an object: the object is
2427
+ * publicly addressed, the actor is one of its addressees, or the actor is
2428
+ * a member of one of them according to `isMember()`.
2429
+ * @param object The object whose audience is checked.
2430
+ * @param actor The actor to check, or `null` if there is no actor. No actor
2431
+ * belongs to the audience of an object that is not public.
2432
+ * @param options Options for checking the audience.
2433
+ * @returns `true` if the actor belongs to the audience of the object.
2434
+ * @internal
2435
+ */
2436
+ async function isInAudience(object, actor, options = {}) {
2437
+ const addressees = getAudienceIds(object);
2438
+ if (addressees.some((id) => PUBLIC_IDS.has(id.href))) return true;
2439
+ const actorId = actor?.id;
2440
+ if (actor == null || actorId == null) return false;
2441
+ const others = [];
2442
+ for (const addressee of addressees) {
2443
+ if (isSameActorId(addressee, actorId)) return true;
2444
+ others.push(addressee);
2445
+ }
2446
+ if (options.isMember == null) return false;
2447
+ for (const addressee of others) if (await options.isMember(addressee, actor)) return true;
2448
+ return false;
2449
+ }
2450
+ //#endregion
2362
2451
  //#region src/federation/inbox.ts
2363
2452
  const Temporal$10 = require_http.Temporal;
2364
2453
  async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
@@ -2983,31 +3072,177 @@ function parseHashlinkGatewayRequest(url) {
2983
3072
  })
2984
3073
  };
2985
3074
  }
3075
+ /**
3076
+ * Resolves a portable inbox, i.e., an `ap:` or `ap+ef61:` URI, to
3077
+ * the compatible identifiers of the inbox on the recipient's gateways.
3078
+ *
3079
+ * The gateways are taken from the recipient's `gateways`, or, if it has no
3080
+ * valid gateway, from the `@gateway` location hints of the inbox URI. Both
3081
+ * come from the recipient object, so they are as trustworthy as its inbox.
3082
+ * Gateways whose origins are in `excludeBaseUris` are skipped.
3083
+ * @param recipient The recipient that owns the inbox.
3084
+ * @param inbox The portable inbox.
3085
+ * @param excludeBaseUris The base URIs whose origins are not delivered to.
3086
+ * @returns The delivery target, or `null` if the inbox is malformed, or no
3087
+ * gateway is left to deliver through.
3088
+ */
3089
+ function resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris) {
3090
+ const logger = (0, _logtape_logtape.getLogger)([
3091
+ "fedify",
3092
+ "federation",
3093
+ "outbox"
3094
+ ]);
3095
+ let portableInbox;
3096
+ try {
3097
+ portableInbox = (0, _fedify_vocab_runtime.canonicalizePortableUri)(inbox.href);
3098
+ } catch (error) {
3099
+ if (!(error instanceof TypeError)) throw error;
3100
+ logger.warn("Skipping the recipient {recipient}, as its portable inbox {inbox} is malformed.", {
3101
+ recipient: recipient.id?.href,
3102
+ inbox: inbox.href,
3103
+ error
3104
+ });
3105
+ return null;
3106
+ }
3107
+ let gateways = (recipient.gateways ?? []).filter(_fedify_vocab_runtime.isGatewayUrl);
3108
+ if (gateways.length < 1) gateways = (0, _fedify_vocab_runtime_internal_portable_dereference.getPortableGatewayCandidates)(inbox);
3109
+ if (gateways.length < 1) {
3110
+ logger.warn("Skipping the recipient {recipient}, as it has no gateway to deliver to its portable inbox {inbox} through.", {
3111
+ recipient: recipient.id?.href,
3112
+ inbox: portableInbox
3113
+ });
3114
+ return null;
3115
+ }
3116
+ const excludedOrigins = new Set(excludeBaseUris?.map((u) => u.origin));
3117
+ const seen = /* @__PURE__ */ new Set();
3118
+ const inboxes = [];
3119
+ for (const gateway of gateways) {
3120
+ if (inboxes.length >= 5) break;
3121
+ if (seen.has(gateway.origin)) continue;
3122
+ seen.add(gateway.origin);
3123
+ if (excludedOrigins.has(gateway.origin)) continue;
3124
+ try {
3125
+ inboxes.push((0, _fedify_vocab_runtime.toCompatibleEf61Id)(inbox, gateway.origin));
3126
+ } catch (error) {
3127
+ if (!(error instanceof TypeError)) throw error;
3128
+ logger.warn("Cannot deliver to the portable inbox {inbox} through the gateway {gateway}, as the inbox cannot be represented as its compatible identifier.", {
3129
+ inbox: portableInbox,
3130
+ gateway: gateway.origin,
3131
+ error
3132
+ });
3133
+ }
3134
+ }
3135
+ if (inboxes.length < 1) {
3136
+ logger.debug("Skipping the recipient {recipient}, as no gateway is left to deliver to its portable inbox {inbox} through.", {
3137
+ recipient: recipient.id?.href,
3138
+ inbox: portableInbox
3139
+ });
3140
+ return null;
3141
+ }
3142
+ return {
3143
+ portableInbox,
3144
+ inboxes
3145
+ };
3146
+ }
3147
+ /**
3148
+ * Merges two ordered lists of compatible inbox URLs, keeping the first URL
3149
+ * for each gateway origin and at most {@link MAX_DELIVERY_GATEWAYS} URLs.
3150
+ * @param a The preferred list.
3151
+ * @param b The other list.
3152
+ * @returns The merged list.
3153
+ */
3154
+ function mergeGatewayInboxes(a, b) {
3155
+ const seen = /* @__PURE__ */ new Set();
3156
+ const merged = [];
3157
+ for (const inbox of [...a, ...b]) {
3158
+ if (merged.length >= 5) break;
3159
+ const { origin } = new URL(inbox);
3160
+ if (seen.has(origin)) continue;
3161
+ seen.add(origin);
3162
+ merged.push(inbox);
3163
+ }
3164
+ return merged;
3165
+ }
3166
+ /**
3167
+ * Gets the part of a delivery's ordering key that tells its destination
3168
+ * apart: the DID of a portable inbox, so that deliveries to it stay in order
3169
+ * whichever gateway they go through, or the origin of any other inbox.
3170
+ * @param inbox The inbox URL.
3171
+ * @param portableInbox The canonical portable inbox, if the inbox is
3172
+ * the compatible identifier of one.
3173
+ * @returns The destination part of the ordering key.
3174
+ */
3175
+ function getOrderingDestination(inbox, portableInbox) {
3176
+ return portableInbox == null ? new URL(inbox).origin : (0, _fedify_vocab_runtime.getFe34Origin)(portableInbox);
3177
+ }
2986
3178
  //#endregion
2987
3179
  //#region src/federation/send.ts
2988
3180
  /**
2989
3181
  * Extracts the inbox URLs from recipients.
3182
+ *
3183
+ * An [FEP-ef61] portable inbox, i.e., an `ap:` or `ap+ef61:` URI, is resolved
3184
+ * to its compatible identifiers on the recipient's gateways, or, if the
3185
+ * recipient has no valid gateway, on the gateways in the `@gateway` location
3186
+ * hints of the inbox URI. The inbox is keyed by the compatible identifier on
3187
+ * the first gateway, and the others are listed in
3188
+ * {@link ExtractedInbox.gatewayInboxes}. A recipient whose portable inbox
3189
+ * has no gateway to deliver through is skipped. The shared inbox of
3190
+ * a recipient with a portable inbox is never preferred, since only a portable
3191
+ * inbox is synchronized between the gateways.
3192
+ *
3193
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2990
3194
  * @param parameters The parameters to extract the inboxes.
2991
3195
  * See also {@link ExtractInboxesParameters}.
2992
3196
  * @returns The inboxes as a map of inbox URL to actor URIs.
2993
3197
  */
2994
3198
  function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) {
2995
3199
  const inboxes = {};
3200
+ const portableKeys = /* @__PURE__ */ new Map();
3201
+ const compatibleKeys = /* @__PURE__ */ new Map();
2996
3202
  for (const recipient of recipients) {
3203
+ if (recipient.id == null) continue;
2997
3204
  let inbox;
2998
3205
  let sharedInbox = false;
2999
- if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) {
3206
+ if (preferSharedInbox && recipient.endpoints?.sharedInbox != null && (recipient.inboxId == null || !require_http.isPortableUri(recipient.inboxId))) {
3000
3207
  inbox = recipient.endpoints.sharedInbox;
3001
3208
  sharedInbox = true;
3002
3209
  } else inbox = recipient.inboxId;
3003
- if (inbox != null && recipient.id != null) {
3004
- if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
3005
- inboxes[inbox.href] ??= {
3210
+ if (inbox == null) continue;
3211
+ if (require_http.isPortableUri(inbox)) {
3212
+ const target = resolvePortableDeliveryTarget(recipient, inbox, excludeBaseUris);
3213
+ if (target == null) continue;
3214
+ const gatewayInboxes = target.inboxes.map((u) => u.href);
3215
+ const existingKeys = /* @__PURE__ */ new Set();
3216
+ const portableKey = portableKeys.get(target.portableInbox);
3217
+ if (portableKey != null) existingKeys.add(portableKey);
3218
+ for (const i of gatewayInboxes) {
3219
+ const k = compatibleKeys.get(i) ?? i;
3220
+ if (Object.hasOwn(inboxes, k)) existingKeys.add(k);
3221
+ }
3222
+ const [key = gatewayInboxes[0], ...otherKeys] = existingKeys;
3223
+ portableKeys.set(target.portableInbox, key);
3224
+ const entry = inboxes[key] ??= {
3006
3225
  actorIds: /* @__PURE__ */ new Set(),
3007
3226
  sharedInbox
3008
3227
  };
3009
- inboxes[inbox.href].actorIds.add(recipient.id.href);
3228
+ for (const k of otherKeys) {
3229
+ for (const actorId of inboxes[k].actorIds) entry.actorIds.add(actorId);
3230
+ delete inboxes[k];
3231
+ }
3232
+ entry.sharedInbox = false;
3233
+ entry.portableInbox = target.portableInbox;
3234
+ entry.gatewayInboxes = mergeGatewayInboxes(entry.gatewayInboxes ?? [key], gatewayInboxes);
3235
+ for (const i of entry.gatewayInboxes) compatibleKeys.set(i, key);
3236
+ entry.actorIds.add(recipient.id.href);
3237
+ continue;
3010
3238
  }
3239
+ if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) continue;
3240
+ const key = compatibleKeys.get(inbox.href) ?? inbox.href;
3241
+ inboxes[key] ??= {
3242
+ actorIds: /* @__PURE__ */ new Set(),
3243
+ sharedInbox
3244
+ };
3245
+ inboxes[key].actorIds.add(recipient.id.href);
3011
3246
  }
3012
3247
  return inboxes;
3013
3248
  }
@@ -3042,6 +3277,38 @@ function sendActivity(options) {
3042
3277
  }
3043
3278
  });
3044
3279
  }
3280
+ /**
3281
+ * Sends an activity to the first of the given inboxes that accepts it, i.e.,
3282
+ * the compatible identifiers of a portable inbox on its gateways, trying them
3283
+ * one after another.
3284
+ * @param parameters The parameters, except for the inbox.
3285
+ * @param inboxes The inboxes to try, in order. Must not be empty.
3286
+ * @throws {Error} The error of the last inbox if no inbox accepts it.
3287
+ */
3288
+ async function sendActivityThroughGateways(parameters, inboxes) {
3289
+ const logger = (0, _logtape_logtape.getLogger)([
3290
+ "fedify",
3291
+ "federation",
3292
+ "outbox"
3293
+ ]);
3294
+ let lastError;
3295
+ for (const [i, inbox] of inboxes.entries()) try {
3296
+ await sendActivity({
3297
+ ...parameters,
3298
+ inbox
3299
+ });
3300
+ return;
3301
+ } catch (error) {
3302
+ lastError = error;
3303
+ if (i < inboxes.length - 1) logger.warn("Failed to send activity {activityId} to {inbox}; trying the next gateway {next}:\n{error}", {
3304
+ activityId: parameters.activityId,
3305
+ inbox: inbox.href,
3306
+ next: inboxes[i + 1].href,
3307
+ error
3308
+ });
3309
+ }
3310
+ throw lastError;
3311
+ }
3045
3312
  const MAX_ERROR_RESPONSE_BODY_BYTES = 1024;
3046
3313
  function getActivityActorId(activity) {
3047
3314
  if (!isRecord(activity)) return void 0;
@@ -3897,6 +4164,15 @@ async function handleObject(request, { values, context, objectDispatcher, author
3897
4164
  * only if its ID canonically matches the requested portable ID and it
3898
4165
  * satisfies the FEP-ef61 proof policy.
3899
4166
  *
4167
+ * FEP-ef61 forbids serving a non-public object to anyone but its intended
4168
+ * audience. If the dispatcher has an authorization predicate, it decides who
4169
+ * may retrieve the object, and the response is marked with
4170
+ * `Cache-Control: private`, as it depends on the requester. Otherwise, only
4171
+ * a publicly addressed object is served, and any other object is responded
4172
+ * to with `404 Not Found`, as if this server did not store it. Actors, and
4173
+ * anything the actor dispatcher returns, are exempt from the latter, as
4174
+ * actors have no audience.
4175
+ *
3900
4176
  * A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
3901
4177
  * Proof made with a key of the DID in its ID. An unsigned tombstone cannot
3902
4178
  * be served as a portable object, so the response is the same as for
@@ -3906,7 +4182,7 @@ async function handleObject(request, { values, context, objectDispatcher, author
3906
4182
  * @param parameters The parameters for handling the portable object.
3907
4183
  * @returns A promise that resolves to an HTTP response.
3908
4184
  */
3909
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
4185
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, kind, onNotFound, onUnauthorized }) {
3910
4186
  const logger = (0, _logtape_logtape.getLogger)([
3911
4187
  "fedify",
3912
4188
  "federation",
@@ -3925,14 +4201,32 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3925
4201
  if (authorizePredicate != null) {
3926
4202
  if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
3927
4203
  }
4204
+ const contextLoader = require_http.getNormalizationContextLoader(context.contextLoader);
3928
4205
  let jsonLd;
3929
- let servedId;
3930
- let result;
4206
+ let root;
3931
4207
  try {
3932
4208
  jsonLd = await object.toJsonLd(context);
3933
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
4209
+ root = await expandJsonLdRoot(jsonLd, contextLoader);
4210
+ } catch (error) {
4211
+ logger.error("Failed to serialize the portable object {portableId}:\n{error}", {
4212
+ portableId: canonicalId,
4213
+ error
4214
+ });
4215
+ return portableObjectInternalServerError(request);
4216
+ }
4217
+ const servedId = root?.["@id"];
4218
+ if (root == null || typeof servedId !== "string" || !isRequestedPortableId(servedId, canonicalId)) {
4219
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
4220
+ return await onNotFound(request);
4221
+ }
4222
+ if (authorizePredicate == null && kind === "object" && !(0, _fedify_vocab.isActor)(object) && !isPubliclyAddressedNode(root)) {
4223
+ 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 });
4224
+ return await onNotFound(request);
4225
+ }
4226
+ let result;
4227
+ try {
3934
4228
  result = await require_http.verifyPortableObjectProof(jsonLd, {
3935
- contextLoader: context.contextLoader,
4229
+ contextLoader,
3936
4230
  documentLoader: context.documentLoader,
3937
4231
  tracerProvider: context.tracerProvider,
3938
4232
  meterProvider: context.meterProvider
@@ -3944,10 +4238,6 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3944
4238
  });
3945
4239
  return portableObjectInternalServerError(request);
3946
4240
  }
3947
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
3948
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
3949
- return await onNotFound(request);
3950
- }
3951
4241
  const tombstone = object instanceof _fedify_vocab.Tombstone;
3952
4242
  if (!result.verified) {
3953
4243
  if (tombstone && result.reason.type === "missingProof") {
@@ -3966,25 +4256,26 @@ async function handlePortableObject(request, { values, context, objectDispatcher
3966
4256
  return portableObjectInternalServerError(request);
3967
4257
  }
3968
4258
  }
4259
+ const headers = new Headers({
4260
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
4261
+ Vary: "Accept"
4262
+ });
4263
+ if (authorizePredicate != null) headers.set("Cache-Control", "private");
3969
4264
  return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
3970
4265
  status: tombstone ? 410 : 200,
3971
- headers: {
3972
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
3973
- Vary: "Accept"
3974
- }
4266
+ headers
3975
4267
  });
3976
4268
  }
3977
4269
  function isRequestedPortableObject(object, canonicalId) {
3978
4270
  return object.id != null && require_http.getCanonicalPortableId(object.id) === canonicalId;
3979
4271
  }
3980
- async function getJsonLdRootId(jsonLd, contextLoader) {
4272
+ async function expandJsonLdRoot(jsonLd, contextLoader) {
3981
4273
  const expanded = await _fedify_vocab_runtime_jsonld.default.expand(jsonLd, {
3982
- documentLoader: require_http.getNormalizationContextLoader(contextLoader),
4274
+ documentLoader: contextLoader,
3983
4275
  keepFreeFloatingNodes: true
3984
4276
  });
3985
4277
  if (expanded.length !== 1) return null;
3986
- const id = expanded[0]["@id"];
3987
- return typeof id === "string" ? id : null;
4278
+ return expanded[0];
3988
4279
  }
3989
4280
  function isRequestedPortableId(id, canonicalId) {
3990
4281
  if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && require_http.getCanonicalPortableId(new URL(id)) === canonicalId;
@@ -7230,6 +7521,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
7230
7521
  function maxDelay(first, second) {
7231
7522
  return Temporal$2.Duration.compare(first, second) >= 0 ? first : second;
7232
7523
  }
7524
+ /**
7525
+ * Gets the fields of a queued message that describe the FEP-ef61 portable
7526
+ * inbox an inbox stands for, if any, omitting the absent ones.
7527
+ */
7528
+ function getPortableMessageFields(portableInbox, gatewayInboxes) {
7529
+ return {
7530
+ ...portableInbox == null ? {} : { portableInbox },
7531
+ ...gatewayInboxes == null ? {} : { gatewayInboxes: [...gatewayInboxes] }
7532
+ };
7533
+ }
7534
+ /**
7535
+ * Sends an activity to an inbox, or, if the inbox is the compatible
7536
+ * identifier of an FEP-ef61 portable inbox on more than one gateway, through
7537
+ * the first gateway that accepts it.
7538
+ */
7539
+ function sendActivityToInbox(inbox, gatewayInboxes, parameters) {
7540
+ if (gatewayInboxes == null || gatewayInboxes.length < 2) return sendActivity({
7541
+ ...parameters,
7542
+ inbox: new URL(inbox)
7543
+ });
7544
+ return sendActivityThroughGateways(parameters, gatewayInboxes.map((i) => new URL(i)));
7545
+ }
7546
+ function parseInstant(value) {
7547
+ if (value == null) return void 0;
7548
+ try {
7549
+ return Temporal$2.Instant.from(value);
7550
+ } catch (error) {
7551
+ if (error instanceof RangeError) return void 0;
7552
+ throw error;
7553
+ }
7554
+ }
7233
7555
  function isTransportDeliveryError(error) {
7234
7556
  return error instanceof _fedify_vocab_runtime.FetchError || require_http.isAbortError(error);
7235
7557
  }
@@ -7810,6 +8132,20 @@ var FederationImpl = class extends FederationBuilderImpl {
7810
8132
  }
7811
8133
  require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
7812
8134
  };
8135
+ const { gatewayInboxes } = message;
8136
+ if (gatewayInboxes != null && gatewayInboxes.length > 1) {
8137
+ await this.#listenGatewayOutboxMessage(_, {
8138
+ ...message,
8139
+ gatewayInboxes
8140
+ }, span, {
8141
+ keys,
8142
+ logData,
8143
+ getActorIds,
8144
+ parseActivity,
8145
+ dropHeldOutboxMessage
8146
+ });
8147
+ return;
8148
+ }
7813
8149
  try {
7814
8150
  const inbox = new URL(message.inbox);
7815
8151
  const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
@@ -8032,6 +8368,308 @@ var FederationImpl = class extends FederationBuilderImpl {
8032
8368
  }
8033
8369
  logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
8034
8370
  }
8371
+ /**
8372
+ * Delivers a queued activity to an FEP-ef61 portable inbox through the first
8373
+ * of its gateways that accepts it, trying them one after another in a single
8374
+ * round, so that the round counts as one attempt for the retry policy, keeps
8375
+ * the message's place in its ordering key, and is replayed as a whole by
8376
+ * queues that retry natively.
8377
+ *
8378
+ * A gateway that fails with a permanent failure status is not tried again.
8379
+ * A gateway whose circuit is open, or which asked to be retried later with
8380
+ * `Retry-After`, is skipped in this round. If no gateway accepts the
8381
+ * activity, the message is held while every remaining gateway is held, and
8382
+ * otherwise retried according to the retry policy.
8383
+ */
8384
+ async #listenGatewayOutboxMessage(contextData, message, span, helpers) {
8385
+ const logger = (0, _logtape_logtape.getLogger)([
8386
+ "fedify",
8387
+ "federation",
8388
+ "outbox"
8389
+ ]);
8390
+ const circuitLogger = (0, _logtape_logtape.getLogger)([
8391
+ "fedify",
8392
+ "federation",
8393
+ "circuit"
8394
+ ]);
8395
+ const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
8396
+ const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
8397
+ const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
8398
+ const survivors = [];
8399
+ let lastError;
8400
+ let lastPermanent;
8401
+ let activity;
8402
+ const getActivity = async () => activity ??= await parseActivity();
8403
+ const drop = async (circuit, remoteHost, inbox, heldSince) => {
8404
+ await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
8405
+ };
8406
+ const heldSince = parseInstant(message.circuitHeldSince);
8407
+ if (circuit != null && heldSince != null && Temporal$2.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal$2.Now.instant()) <= 0) {
8408
+ const inbox = new URL(message.gatewayInboxes[0]);
8409
+ await drop(circuit, require_http.getRemoteHost(inbox), inbox, heldSince);
8410
+ return;
8411
+ }
8412
+ for (const inboxHref of message.gatewayInboxes) {
8413
+ const inbox = new URL(inboxHref);
8414
+ const remoteHost = require_http.getRemoteHost(inbox);
8415
+ const now = Temporal$2.Now.instant();
8416
+ const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
8417
+ if (notBefore != null && Temporal$2.Instant.compare(notBefore, now) > 0) {
8418
+ survivors.push({
8419
+ inbox: inboxHref,
8420
+ holdDelay: now.until(notBefore),
8421
+ notBefore
8422
+ });
8423
+ continue;
8424
+ }
8425
+ let decision;
8426
+ if (circuit != null) try {
8427
+ decision = await circuit.beforeSend(remoteHost, message);
8428
+ } catch (circuitError) {
8429
+ circuitLogger.error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
8430
+ ...logData,
8431
+ remoteHost,
8432
+ error: circuitError
8433
+ });
8434
+ }
8435
+ if (decision != null && circuit != null) {
8436
+ if (decision.type === "drop") {
8437
+ await drop(circuit, remoteHost, inbox, decision.heldSince);
8438
+ return;
8439
+ } else if (decision.type === "hold") {
8440
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
8441
+ survivors.push({
8442
+ inbox: inboxHref,
8443
+ holdDelay: decision.delay,
8444
+ circuitHeld: true
8445
+ });
8446
+ continue;
8447
+ } else if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
8448
+ }
8449
+ try {
8450
+ await sendActivity({
8451
+ allowPrivateAddress: this.allowPrivateAddress,
8452
+ keys,
8453
+ activity: message.activity,
8454
+ activityId: message.activityId,
8455
+ activityType: message.activityType,
8456
+ inbox,
8457
+ sharedInbox: message.sharedInbox,
8458
+ headers: new Headers(message.headers),
8459
+ specDeterminer,
8460
+ meterProvider: this.meterProvider,
8461
+ tracerProvider: this.tracerProvider
8462
+ });
8463
+ } catch (error) {
8464
+ lastError = error;
8465
+ const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
8466
+ let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
8467
+ let retryAt;
8468
+ if (retryAfter != null) try {
8469
+ retryAt = Temporal$2.Now.instant().add(retryAfter);
8470
+ } catch (e) {
8471
+ if (!(e instanceof RangeError)) throw e;
8472
+ retryAfter = void 0;
8473
+ }
8474
+ let postDecision;
8475
+ if (circuit != null) try {
8476
+ if (error instanceof SendActivityError) {
8477
+ const { statusCode } = error;
8478
+ const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await circuit.recordReachableFailure(remoteHost) : statusCode >= 500 ? await circuit.recordFailure(remoteHost) : void 0;
8479
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8480
+ } else if (isTransportDeliveryError(error)) {
8481
+ const stateChange = await circuit.recordFailure(remoteHost);
8482
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8483
+ }
8484
+ if (!isPermanentFailure) postDecision = await circuit.beforeSend(remoteHost, message);
8485
+ } catch (circuitError) {
8486
+ circuitLogger.error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
8487
+ ...logData,
8488
+ remoteHost,
8489
+ error: circuitError
8490
+ });
8491
+ }
8492
+ span.addEvent("activitypub.delivery.failed", {
8493
+ "activitypub.remote.host": remoteHost,
8494
+ "activitypub.delivery.attempt": message.attempt,
8495
+ "activitypub.delivery.permanent_failure": isPermanentFailure,
8496
+ ...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
8497
+ });
8498
+ try {
8499
+ await this.onOutboxError?.(error, await getActivity());
8500
+ } catch (handlerError) {
8501
+ logger.error("An unexpected error occurred in onError handler:\n{error}", {
8502
+ ...logData,
8503
+ inbox: inboxHref,
8504
+ error: handlerError
8505
+ });
8506
+ }
8507
+ if (isPermanentFailure) {
8508
+ logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
8509
+ ...logData,
8510
+ inbox: inboxHref,
8511
+ status: error.statusCode
8512
+ });
8513
+ lastPermanent = {
8514
+ error,
8515
+ inbox
8516
+ };
8517
+ continue;
8518
+ }
8519
+ if (postDecision?.type === "drop" && circuit != null) {
8520
+ await drop(circuit, remoteHost, inbox, postDecision.heldSince);
8521
+ return;
8522
+ } else if (postDecision?.type === "hold") {
8523
+ recordCircuitBreakerHeldSpanEvent(span, remoteHost, postDecision.state);
8524
+ survivors.push({
8525
+ inbox: inboxHref,
8526
+ holdDelay: retryAfter == null ? postDecision.delay : maxDelay(postDecision.delay, retryAfter),
8527
+ circuitHeld: true,
8528
+ postFailure: true,
8529
+ notBefore: retryAt,
8530
+ retryAfter
8531
+ });
8532
+ } else survivors.push({
8533
+ inbox: inboxHref,
8534
+ notBefore: retryAt,
8535
+ retryAfter
8536
+ });
8537
+ logger.warn("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); trying the next gateway if any:\n{error}", {
8538
+ ...logData,
8539
+ inbox: inboxHref,
8540
+ error
8541
+ });
8542
+ continue;
8543
+ }
8544
+ if (circuit != null) try {
8545
+ const stateChange = await circuit.recordSuccess(remoteHost);
8546
+ if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
8547
+ } catch (error) {
8548
+ circuitLogger.error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
8549
+ ...logData,
8550
+ remoteHost,
8551
+ error
8552
+ });
8553
+ }
8554
+ logger.info("Successfully sent activity {activityId} to {inbox}.", {
8555
+ ...logData,
8556
+ inbox: inboxHref
8557
+ });
8558
+ return;
8559
+ }
8560
+ if (lastError != null) span.setStatus({
8561
+ code: _opentelemetry_api.SpanStatusCode.ERROR,
8562
+ message: String(lastError)
8563
+ });
8564
+ if (survivors.length < 1) {
8565
+ const { error, inbox } = lastPermanent;
8566
+ this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
8567
+ logger.warn("Permanent delivery failure for activity {activityId} to every gateway of {portableInbox}; not retrying.", {
8568
+ ...logData,
8569
+ portableInbox: message.portableInbox
8570
+ });
8571
+ if (this.outboxPermanentFailureHandler != null) {
8572
+ const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
8573
+ try {
8574
+ await this.outboxPermanentFailureHandler(ctx, {
8575
+ reason: "http",
8576
+ inbox,
8577
+ activity: await getActivity(),
8578
+ error,
8579
+ statusCode: error.statusCode,
8580
+ actorIds: getActorIds()
8581
+ });
8582
+ } catch (handlerError) {
8583
+ logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
8584
+ ...logData,
8585
+ error: handlerError
8586
+ });
8587
+ }
8588
+ }
8589
+ require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
8590
+ return;
8591
+ }
8592
+ const now = Temporal$2.Now.instant();
8593
+ const gatewayNotBefore = {};
8594
+ for (const survivor of survivors) if (survivor.notBefore != null && Temporal$2.Instant.compare(survivor.notBefore, now) > 0) gatewayNotBefore[survivor.inbox] = survivor.notBefore.toString();
8595
+ const { circuitHeld: _circuitHeld, gatewayNotBefore: _gatewayNotBefore, ...rest } = message;
8596
+ const nextMessage = {
8597
+ ...rest,
8598
+ inbox: survivors[0].inbox,
8599
+ gatewayInboxes: survivors.map((s) => s.inbox),
8600
+ ...globalThis.Object.keys(gatewayNotBefore).length < 1 ? {} : { gatewayNotBefore }
8601
+ };
8602
+ const { outboxQueue } = this;
8603
+ const policyDelay = this.outboxRetryPolicy({
8604
+ elapsedTime: Temporal$2.Instant.from(message.started).until(now),
8605
+ attempts: message.attempt
8606
+ });
8607
+ if (outboxQueue != null && survivors.every((s) => s.holdDelay != null) && (policyDelay != null || !survivors.some((s) => s.postFailure))) {
8608
+ let delay = survivors.map((s) => s.holdDelay).reduce((a, b) => Temporal$2.Duration.compare(a, b) <= 0 ? a : b);
8609
+ let heldMessage = nextMessage;
8610
+ if (survivors.some((s) => s.circuitHeld)) {
8611
+ const since = heldSince ?? now;
8612
+ if (circuit != null) delay = circuit.capHeldDelay(since, delay);
8613
+ heldMessage = {
8614
+ ...nextMessage,
8615
+ circuitHeld: true,
8616
+ circuitHeldSince: since.toString()
8617
+ };
8618
+ }
8619
+ logger.warn("Failed to send activity {activityId} to {portableInbox}; holding it until one of its gateways can be tried again.", {
8620
+ ...logData,
8621
+ portableInbox: message.portableInbox
8622
+ });
8623
+ await outboxQueue.enqueue(heldMessage, {
8624
+ delay: clampNegativeDelay(delay),
8625
+ orderingKey: message.orderingKey
8626
+ });
8627
+ this.metrics.recordQueueTaskEnqueued({
8628
+ role: "outbox",
8629
+ queue: outboxQueue,
8630
+ activityType: heldMessage.activityType
8631
+ }, heldMessage.attempt);
8632
+ return;
8633
+ }
8634
+ const hasRetryAfter = survivors.some((s) => s.retryAfter != null);
8635
+ if (outboxQueue?.nativeRetrial && !hasRetryAfter && survivors.length === message.gatewayInboxes.length && globalThis.Object.keys(gatewayNotBefore).length < 1) {
8636
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox}; backend will handle retry:\n{error}", {
8637
+ ...logData,
8638
+ portableInbox: message.portableInbox,
8639
+ error: lastError
8640
+ });
8641
+ throw lastError;
8642
+ }
8643
+ if (policyDelay == null || outboxQueue == null) {
8644
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
8645
+ ...logData,
8646
+ portableInbox: message.portableInbox,
8647
+ error: lastError
8648
+ });
8649
+ require_http.recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
8650
+ return;
8651
+ }
8652
+ const delay = survivors.map((s) => s.holdDelay ?? s.retryAfter ?? policyDelay).reduce((a, b) => Temporal$2.Duration.compare(a, b) <= 0 ? a : b);
8653
+ const retryMessage = {
8654
+ ...nextMessage,
8655
+ attempt: message.attempt + 1
8656
+ };
8657
+ logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} (attempt #{attempt}); retry...:\n{error}", {
8658
+ ...logData,
8659
+ portableInbox: message.portableInbox,
8660
+ error: lastError
8661
+ });
8662
+ await outboxQueue.enqueue(retryMessage, {
8663
+ delay: clampNegativeDelay(delay),
8664
+ orderingKey: message.orderingKey
8665
+ });
8666
+ this.metrics.recordQueueTaskEnqueued({
8667
+ role: "outbox",
8668
+ queue: outboxQueue,
8669
+ activityType: retryMessage.activityType
8670
+ }, retryMessage.attempt);
8671
+ require_http.recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
8672
+ }
8035
8673
  async #listenInboxMessage(ctxData, message, span, onActivityType) {
8036
8674
  const logger = (0, _logtape_logtape.getLogger)([
8037
8675
  "fedify",
@@ -8461,13 +9099,12 @@ var FederationImpl = class extends FederationBuilderImpl {
8461
9099
  activity: jsonLd
8462
9100
  });
8463
9101
  const promises = [];
8464
- for (const inbox in inboxes) promises.push(sendActivity({
9102
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
8465
9103
  allowPrivateAddress: this.allowPrivateAddress,
8466
9104
  keys,
8467
9105
  activity: jsonLd,
8468
9106
  activityId: activity.id?.href,
8469
9107
  activityType: (0, _fedify_vocab.getTypeId)(activity).href,
8470
- inbox: new URL(inbox),
8471
9108
  sharedInbox: inboxes[inbox].sharedInbox,
8472
9109
  headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
8473
9110
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
@@ -8494,8 +9131,8 @@ var FederationImpl = class extends FederationBuilderImpl {
8494
9131
  _opentelemetry_api.propagation.inject(_opentelemetry_api.context.active(), carrier);
8495
9132
  const messages = [];
8496
9133
  for (const inbox in inboxes) {
8497
- const inboxOrigin = new URL(inbox).origin;
8498
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${inboxOrigin}`;
9134
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
9135
+ const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
8499
9136
  const message = {
8500
9137
  type: "outbox",
8501
9138
  id: crypto.randomUUID(),
@@ -8511,6 +9148,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8511
9148
  attempt: 0,
8512
9149
  headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
8513
9150
  orderingKey: messageOrderingKey,
9151
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
8514
9152
  traceContext: carrier
8515
9153
  };
8516
9154
  messages.push({
@@ -8960,6 +9598,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8960
9598
  objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
8961
9599
  authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
8962
9600
  canonicalId: portable.canonicalId,
9601
+ kind: "actor",
8963
9602
  onUnauthorized,
8964
9603
  onNotFound
8965
9604
  });
@@ -8980,6 +9619,7 @@ var FederationImpl = class extends FederationBuilderImpl {
8980
9619
  objectDispatcher: callbacks?.dispatcher,
8981
9620
  authorizePredicate: callbacks?.authorizePredicate,
8982
9621
  canonicalId: portable.canonicalId,
9622
+ kind: "object",
8983
9623
  onUnauthorized,
8984
9624
  onNotFound
8985
9625
  });
@@ -9976,9 +10616,10 @@ var ContextImpl = class ContextImpl {
9976
10616
  id: crypto.randomUUID(),
9977
10617
  baseUrl: this.origin,
9978
10618
  keys: keyJwkPairs,
9979
- inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
10619
+ inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
9980
10620
  actorIds: [...actorIds],
9981
- sharedInbox
10621
+ sharedInbox,
10622
+ ...getPortableMessageFields(portableInbox, gatewayInboxes)
9982
10623
  }])),
9983
10624
  activity: activityJsonLd,
9984
10625
  activityId: activity.id?.href,
@@ -10275,6 +10916,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
10275
10916
  throw error;
10276
10917
  }
10277
10918
  }
10919
+ async isSignedByAudience(object, options = {}) {
10920
+ return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
10921
+ }
10278
10922
  };
10279
10923
  function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
10280
10924
  return ctx.tracerProvider.getTracer(require_http.name, require_http.version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
@@ -10362,13 +11006,12 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10362
11006
  if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
10363
11007
  else logger.debug("Forwarding activity immediately without queue since queue is not set.");
10364
11008
  const promises = [];
10365
- for (const inbox in inboxes) promises.push(sendActivity({
11009
+ for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
10366
11010
  allowPrivateAddress: ctx.federation.allowPrivateAddress,
10367
11011
  keys,
10368
11012
  activity: ctx.activity,
10369
11013
  activityId: ctx.activityId,
10370
11014
  activityType: ctx.activityType,
10371
- inbox: new URL(inbox),
10372
11015
  sharedInbox: inboxes[inbox].sharedInbox,
10373
11016
  meterProvider: ctx.meterProvider,
10374
11017
  tracerProvider: ctx.tracerProvider,
@@ -10396,7 +11039,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10396
11039
  const started = (/* @__PURE__ */ new Date()).toISOString();
10397
11040
  const messages = [];
10398
11041
  for (const inbox in inboxes) {
10399
- const inboxUrl = new URL(inbox);
11042
+ const { portableInbox, gatewayInboxes } = inboxes[inbox];
10400
11043
  const message = {
10401
11044
  type: "outbox",
10402
11045
  id: crypto.randomUUID(),
@@ -10411,7 +11054,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
10411
11054
  started,
10412
11055
  attempt: 0,
10413
11056
  headers: {},
10414
- orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${inboxUrl.origin}`,
11057
+ orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
11058
+ ...getPortableMessageFields(portableInbox, gatewayInboxes),
10415
11059
  traceContext: carrier
10416
11060
  };
10417
11061
  messages.push({