@fedify/fedify 2.4.0-dev.2071 → 2.4.0-dev.2073

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 (83) hide show
  1. package/dist/{builder-DVQ0yhKJ.mjs → builder-CVHpPAjK.mjs} +2 -2
  2. package/dist/compat/mod.d.cts +1 -1
  3. package/dist/compat/mod.d.ts +1 -1
  4. package/dist/compat/transformers.test.mjs +1 -1
  5. package/dist/{compound-proof-KmXDfWFp.mjs → compound-proof-B5Skp-vE.mjs} +1 -1
  6. package/dist/{context-DsB9kF6q.d.ts → context-BSywYspi.d.ts} +109 -1
  7. package/dist/{context-DijrgHDe.d.cts → context-CZjZYXFC.d.cts} +109 -1
  8. package/dist/{context-DG5QeQ8W.mjs → context-DlZhxdFT.mjs} +2 -1
  9. package/dist/{deno-B-NtYZVW.mjs → deno-LYTbBhYV.mjs} +1 -1
  10. package/dist/{docloader-C6yOVoaF.mjs → docloader-C0SWQylk.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +3 -3
  21. package/dist/federation/mod.d.ts +3 -3
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  25. package/dist/federation/portable-inbox.test.d.mts +2 -0
  26. package/dist/federation/portable-inbox.test.mjs +546 -0
  27. package/dist/federation/portable.test.mjs +3 -3
  28. package/dist/federation/send.test.mjs +3 -3
  29. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  30. package/dist/federation/tasks/tasks.test.mjs +3 -3
  31. package/dist/federation/temporal.test.mjs +1 -1
  32. package/dist/federation/webfinger.test.mjs +2 -2
  33. package/dist/{federation-CUiHc7dP.cjs → federation-B1aeTKKN.cjs} +468 -88
  34. package/dist/{http-DKWi8RjI.mjs → http-C7qHlv-b.mjs} +4 -4
  35. package/dist/{http-CuLWIJdf.cjs → http-DGk8XI38.cjs} +1 -1
  36. package/dist/{http-BoWBwC0g.js → http-vfTpzGEq.js} +1 -1
  37. package/dist/{key-lfcx_NaE.mjs → key-BYXnTGro.mjs} +2 -2
  38. package/dist/{kv-cache-BdnmnAVc.js → kv-cache-DCJuwdnB.js} +1 -1
  39. package/dist/{kv-cache-d8EjO29d.mjs → kv-cache-DEH5jyND.mjs} +1 -1
  40. package/dist/{kv-cache-DE8kSNQU.cjs → kv-cache-DvqkKNIj.cjs} +1 -1
  41. package/dist/{ld-jyjdrWjG.mjs → ld-DT3e2hOv.mjs} +3 -3
  42. package/dist/{metrics-B5Rei-HC.mjs → metrics-DvlvYpq_.mjs} +1 -1
  43. package/dist/{middleware-CdVnCDXD.mjs → middleware-BJcez_E3.mjs} +1 -1
  44. package/dist/{middleware-CAObohey.js → middleware-CCbyUF4c.js} +417 -38
  45. package/dist/{middleware-CPx4ZWwt.mjs → middleware-CpyCNOW_.mjs} +423 -48
  46. package/dist/{mod-DMkflQdF.d.ts → mod-B2h2KY0q.d.ts} +1 -1
  47. package/dist/{mod-DTAaBwiY.d.cts → mod-JzkaEXsi.d.cts} +1 -1
  48. package/dist/mod.cjs +5 -5
  49. package/dist/mod.d.cts +4 -4
  50. package/dist/mod.d.ts +4 -4
  51. package/dist/mod.js +5 -5
  52. package/dist/nodeinfo/handler.test.mjs +2 -2
  53. package/dist/{owner-ByqNQVaa.mjs → owner-CDsVeZI7.mjs} +3 -3
  54. package/dist/{owner-BXufwAzm.cjs → owner-DCIH7Iq0.cjs} +1 -1
  55. package/dist/{owner-1TFakcTF.js → owner-DfGz0cXm.js} +1 -1
  56. package/dist/{portable-key-Cw_ow5vn.mjs → portable-key-DCyKGCQd.mjs} +2 -2
  57. package/dist/{proof-B6gwdhN5.mjs → proof-CxsVG6vM.mjs} +4 -4
  58. package/dist/{send-DsK7xevO.mjs → send-BZJ7NsVV.mjs} +8 -4
  59. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  60. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  61. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  62. package/dist/sig/compound-proof.test.mjs +2 -2
  63. package/dist/sig/http.test.mjs +2 -2
  64. package/dist/sig/key.test.mjs +1 -1
  65. package/dist/sig/ld.test.mjs +2 -2
  66. package/dist/sig/mod.cjs +3 -3
  67. package/dist/sig/mod.js +3 -3
  68. package/dist/sig/owner.test.mjs +1 -1
  69. package/dist/sig/portable-collection.test.mjs +2 -2
  70. package/dist/sig/portable-key.test.mjs +3 -3
  71. package/dist/sig/proof.test.mjs +3 -3
  72. package/dist/{sig-TTn9wYrE.cjs → sig-C6TnADze.cjs} +2 -2
  73. package/dist/{sig-BR_Z6mpY.js → sig-Cs9_ecdh.js} +2 -2
  74. package/dist/{temporal-Bw5jtrBY.mjs → temporal-CLzSfIOV.mjs} +1 -1
  75. package/dist/testing/kv.test.mjs +1 -1
  76. package/dist/testing/mod.d.mts +108 -0
  77. package/dist/testing/mod.mjs +2 -2
  78. package/dist/{testing-By1YYsIK.mjs → testing-CO_cEvQc.mjs} +1 -1
  79. package/dist/utils/docloader.test.mjs +2 -2
  80. package/dist/utils/kv-cache.test.mjs +1 -1
  81. package/dist/utils/mod.cjs +1 -1
  82. package/dist/utils/mod.js +1 -1
  83. package/package.json +6 -6
@@ -2,10 +2,10 @@ 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 recordCollectionPageItems, C as hasSignature, D as signJsonLd, E as isInvalidUrlTypeError, H as hasPortableActor, J as getRemoteHost, K as getDurationMs, L as importJwk, M as exportJwk, O as verifyCompactJsonLd, Q as recordCollectionDispatchDuration, R as validateCryptoKey, S as getNormalizationContextLoader, T as isClearlyMalformedContextReference, U as isCompatibleKeyId, V as getGatewayKeyBase, W as isPortableUri, X as isAbortError, Y as instrumentDocumentLoader, Z as recordCircuitBreakerStateChange, _ as assertSafeJsonLd, a as verifyRequestDetailed, at as recordOutboxActivity, c as hasProofLike, ct as registerQueueDepthGauge, d as verifyObject, et as recordCollectionRequest, f as verifyPortableObjectProof, g as InvalidContextReferenceError, ht as version, i as verifyRequest, it as recordInboxActivity, j as wrapContextLoaderForJsonLd, l as signObject, lt as normalizeOutgoingActivityJsonLd, mt as name, n as parseRfc9421SignatureInput, ot as recordOutboxEnqueue, p as verifyPortableObjectProofPolicy, q as getFederationMetrics, rt as recordFanoutRecipients, st as recordWebFingerHandle, t as doubleKnock, tt as recordCollectionTotalItems, u as verifyMapLocalProof, ut as formatAcceptSignature, w as hasSignatureLike, x as detachSignature, y as compactJsonLd } from "./http-BoWBwC0g.js";
5
+ import { $ as recordCollectionPageItems, C as hasSignature, D as signJsonLd, E as isInvalidUrlTypeError, H as hasPortableActor, J as getRemoteHost, K as getDurationMs, L as importJwk, M as exportJwk, O as verifyCompactJsonLd, Q as recordCollectionDispatchDuration, R as validateCryptoKey, S as getNormalizationContextLoader, T as isClearlyMalformedContextReference, U as isCompatibleKeyId, V as getGatewayKeyBase, W as isPortableUri, X as isAbortError, Y as instrumentDocumentLoader, Z as recordCircuitBreakerStateChange, _ as assertSafeJsonLd, a as verifyRequestDetailed, at as recordOutboxActivity, c as hasProofLike, ct as registerQueueDepthGauge, d as verifyObject, et as recordCollectionRequest, f as verifyPortableObjectProof, g as InvalidContextReferenceError, ht as version, i as verifyRequest, it as recordInboxActivity, j as wrapContextLoaderForJsonLd, l as signObject, lt as normalizeOutgoingActivityJsonLd, mt as name, n as parseRfc9421SignatureInput, ot as recordOutboxEnqueue, p as verifyPortableObjectProofPolicy, q as getFederationMetrics, rt as recordFanoutRecipients, st as recordWebFingerHandle, t as doubleKnock, tt as recordCollectionTotalItems, u as verifyMapLocalProof, ut as formatAcceptSignature, w as hasSignatureLike, x as detachSignature, y as compactJsonLd } from "./http-vfTpzGEq.js";
6
6
  import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
7
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-1TFakcTF.js";
8
- import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-BdnmnAVc.js";
7
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DfGz0cXm.js";
8
+ import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DCJuwdnB.js";
9
9
  import { getLogger, withContext } from "@logtape/logtape";
10
10
  import { Router, RouterError, assertPath } from "@fedify/uri-template";
11
11
  import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
@@ -2633,13 +2633,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
2633
2633
  * @param parameters The parameters for handling the collection.
2634
2634
  * @returns A promise that resolves to an HTTP response.
2635
2635
  */
2636
- async function handleCollection(request, { name: name$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2637
- const spanName = name$1.trim().replace(/\s+/g, "_");
2636
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
2637
+ const spanName = name$2.trim().replace(/\s+/g, "_");
2638
2638
  tracerProvider = tracerProvider ?? trace.getTracerProvider();
2639
2639
  const tracer = tracerProvider.getTracer(name, version);
2640
2640
  const cursor = new URL(request.url).searchParams.get("cursor");
2641
2641
  const metricBase = {
2642
- kind: getCollectionMetricKind(name$1),
2642
+ kind: getCollectionMetricKind(name$2),
2643
2643
  page: cursor != null,
2644
2644
  dispatcher: "built_in"
2645
2645
  };
@@ -2680,7 +2680,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2680
2680
  span.setStatus({ code: SpanStatusCode.ERROR });
2681
2681
  return await onNotFound(request);
2682
2682
  }
2683
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2683
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2684
2684
  itemCount = items.length;
2685
2685
  span.setAttribute("fedify.collection.items", itemCount);
2686
2686
  return items;
@@ -2720,7 +2720,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2720
2720
  } else {
2721
2721
  const uri = new URL(baseUri);
2722
2722
  uri.searchParams.set("cursor", cursor);
2723
- const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$1}`, {
2723
+ const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
2724
2724
  kind: SpanKind.SERVER,
2725
2725
  attributes: {
2726
2726
  "activitypub.collection.id": uri.href,
@@ -2736,7 +2736,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
2736
2736
  span.setStatus({ code: SpanStatusCode.ERROR });
2737
2737
  return await onNotFound(request);
2738
2738
  }
2739
- const items = filterCollectionItems(page.items, name$1, filterPredicate);
2739
+ const items = filterCollectionItems(page.items, name$2, filterPredicate);
2740
2740
  itemCount = items.length;
2741
2741
  span.setAttribute("fedify.collection.items", itemCount);
2742
2742
  return {
@@ -3242,7 +3242,7 @@ async function handleInbox(request, options) {
3242
3242
  * @returns A promise that resolves to an HTTP response.
3243
3243
  */
3244
3244
  async function handleInboxInternal(request, parameters, span) {
3245
- const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
3245
+ const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
3246
3246
  const logger = getLogger([
3247
3247
  "fedify",
3248
3248
  "federation",
@@ -3255,7 +3255,7 @@ async function handleInboxInternal(request, parameters, span) {
3255
3255
  message: "Actor dispatcher is not set."
3256
3256
  });
3257
3257
  return await onNotFound(request);
3258
- } else if (recipient != null) {
3258
+ } else if (recipient != null && portableInbox == null) {
3259
3259
  const actor = await actorDispatcher(ctx, recipient);
3260
3260
  if (actor == null || actor instanceof Tombstone) {
3261
3261
  logger.error("Actor {recipient} not found.", { recipient });
@@ -3717,6 +3717,19 @@ async function handleInboxInternal(request, parameters, span) {
3717
3717
  tracerProvider,
3718
3718
  idempotencyStrategy: parameters.idempotencyStrategy
3719
3719
  });
3720
+ if (portableInbox != null && activity.id != null && PORTABLE_INBOX_FORWARDABLE_RESULTS.has(routeResult)) if (skipSignatureVerification || !(ldSigVerified || proofVerified)) logger.debug("Not forwarding activity {activityId} to the other gateways of the portable inbox {inbox}, as it is not authenticated by its own proof.", {
3721
+ activityId: activity.id.href,
3722
+ inbox: portableInbox.recipient.canonicalInboxId
3723
+ });
3724
+ else try {
3725
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href);
3726
+ } catch (error) {
3727
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
3728
+ activityId: activity.id.href,
3729
+ inbox: portableInbox.recipient.canonicalInboxId,
3730
+ error
3731
+ });
3732
+ }
3720
3733
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
3721
3734
  status: 202,
3722
3735
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -3742,6 +3755,12 @@ async function handleInboxInternal(request, parameters, span) {
3742
3755
  headers: { "Content-Type": "text/plain; charset=utf-8" }
3743
3756
  });
3744
3757
  }
3758
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
3759
+ "success",
3760
+ "enqueued",
3761
+ "alreadyProcessed",
3762
+ "unsupportedActivity"
3763
+ ]);
3745
3764
  /**
3746
3765
  * Handles a custom collection request.
3747
3766
  * @template TItem The type of items in the collection.
@@ -3864,8 +3883,8 @@ var CustomCollectionHandler = class {
3864
3883
  * @param CollectionPage The CollectionPage constructor.
3865
3884
  * @param filterPredicate Optional filter predicate for items.
3866
3885
  */
3867
- constructor(name$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3868
- this.name = name$2;
3886
+ constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
3887
+ this.name = name$1;
3869
3888
  this.values = values;
3870
3889
  this.context = context;
3871
3890
  this.callbacks = callbacks;
@@ -4533,7 +4552,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
4533
4552
  rsaKey = key;
4534
4553
  break;
4535
4554
  }
4536
- if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4555
+ if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
4556
+ activityId,
4557
+ inbox: inbox.href
4558
+ });
4559
+ else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4537
4560
  inbox: inbox.href,
4538
4561
  keys: keys.map((pair) => ({
4539
4562
  keyId: pair.keyId.href,
@@ -4875,6 +4898,251 @@ function serializeDataPointType(dataPointType) {
4875
4898
  }
4876
4899
  }
4877
4900
  //#endregion
4901
+ //#region src/federation/portable-inbox.ts
4902
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
4903
+ /**
4904
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
4905
+ * validates them.
4906
+ * @param options The options.
4907
+ * @returns The resolved options.
4908
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
4909
+ * is not positive, if `deadline` is negative or longer
4910
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
4911
+ * if either duration has calendar units, i.e., weeks,
4912
+ * months, or years, whose length depends on the date.
4913
+ */
4914
+ function resolvePortableInboxForwardingOptions(options = {}) {
4915
+ const maxTargets = options.maxTargets ?? 10;
4916
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
4917
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
4918
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
4919
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
4920
+ const deadlineMs = deadline.total("millisecond");
4921
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
4922
+ return {
4923
+ maxTargets,
4924
+ ttl,
4925
+ deadline
4926
+ };
4927
+ }
4928
+ /**
4929
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
4930
+ * of the given actor, which the actor dispatcher returned for the identifier
4931
+ * in the requested inbox path.
4932
+ *
4933
+ * The actor document comes from the application, so it is trusted as is and
4934
+ * its proof is not verified here.
4935
+ * @param actor The actor the actor dispatcher returned.
4936
+ * @param options The requested DID, the canonical form of the requested inbox
4937
+ * ID, and the origin of this server.
4938
+ * @returns The resolution.
4939
+ */
4940
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
4941
+ if (actor == null) return {
4942
+ status: "rejected",
4943
+ reason: "notFound"
4944
+ };
4945
+ const id = actor.id;
4946
+ if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
4947
+ status: "rejected",
4948
+ reason: "notPortable"
4949
+ };
4950
+ if (!isSameAuthority(id, authority)) return {
4951
+ status: "rejected",
4952
+ reason: "authorityMismatch"
4953
+ };
4954
+ const inboxId = actor.inboxId;
4955
+ if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
4956
+ status: "rejected",
4957
+ reason: "inboxMismatch"
4958
+ };
4959
+ const gateways = actor.gateways;
4960
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
4961
+ status: "rejected",
4962
+ reason: "notGateway"
4963
+ };
4964
+ return {
4965
+ status: "accepted",
4966
+ recipient: {
4967
+ inboxId,
4968
+ canonicalInboxId,
4969
+ gateways
4970
+ }
4971
+ };
4972
+ }
4973
+ function isSameAuthority(id, authority) {
4974
+ try {
4975
+ return getFe34Origin(id) === authority;
4976
+ } catch (error) {
4977
+ if (error instanceof TypeError) return false;
4978
+ throw error;
4979
+ }
4980
+ }
4981
+ function canonicalize(iri) {
4982
+ try {
4983
+ return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
4984
+ } catch (error) {
4985
+ if (error instanceof TypeError) return null;
4986
+ throw error;
4987
+ }
4988
+ }
4989
+ /**
4990
+ * Forwards an activity received in a portable inbox to the inboxes of the same
4991
+ * portable actor on its other gateways, as FEP-ef61 recommends.
4992
+ *
4993
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
4994
+ * each target gateway is claimed with an atomic compare-and-swap before the
4995
+ * activity is handed off, and a claim is never released, even if the hand-off
4996
+ * fails, since a failed hand-off does not prove that nothing was delivered.
4997
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
4998
+ * forwarded.
4999
+ *
5000
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
5001
+ * gateways authenticate the activity by its own proof.
5002
+ * @param parameters The parameters.
5003
+ * @returns The target inbox URLs that the activity was handed off to.
5004
+ */
5005
+ async function forwardPortableInboxActivity(parameters) {
5006
+ const logger = getLogger([
5007
+ "fedify",
5008
+ "federation",
5009
+ "inbox"
5010
+ ]);
5011
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
5012
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
5013
+ if (kv.cas == null || maxTargets < 1) return [];
5014
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
5015
+ const targets = getForwardingTargets(recipient, excludedOrigins);
5016
+ if (targets.length > maxTargets) {
5017
+ logger.warn("The portable actor that owns the inbox {inbox} has more than {max} other gateways; the activity {activityId} is forwarded only to the first {max} of them.", {
5018
+ inbox: recipient.canonicalInboxId,
5019
+ activityId: activityId.href,
5020
+ max: maxTargets
5021
+ });
5022
+ targets.length = maxTargets;
5023
+ }
5024
+ const claimed = [];
5025
+ for (const { gateway, inbox } of targets) {
5026
+ const key = [
5027
+ ...kvPrefix,
5028
+ recipient.canonicalInboxId,
5029
+ canonicalActivityId,
5030
+ gateway
5031
+ ];
5032
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
5033
+ }
5034
+ if (claimed.length < 1) return [];
5035
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
5036
+ activityId: activityId.href,
5037
+ inbox: recipient.canonicalInboxId,
5038
+ targets: claimed.map((t) => t.href)
5039
+ });
5040
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
5041
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
5042
+ return claimed;
5043
+ }
5044
+ function getForwardingTargets(recipient, excludedOrigins) {
5045
+ const logger = getLogger([
5046
+ "fedify",
5047
+ "federation",
5048
+ "inbox"
5049
+ ]);
5050
+ const seen = new Set(excludedOrigins);
5051
+ const targets = [];
5052
+ for (const gateway of recipient.gateways) {
5053
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
5054
+ seen.add(gateway.origin);
5055
+ let inbox;
5056
+ try {
5057
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
5058
+ } catch (error) {
5059
+ if (!(error instanceof TypeError)) throw error;
5060
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
5061
+ gateway: gateway.origin,
5062
+ inbox: recipient.canonicalInboxId
5063
+ });
5064
+ continue;
5065
+ }
5066
+ targets.push({
5067
+ gateway: gateway.origin,
5068
+ inbox
5069
+ });
5070
+ }
5071
+ return targets;
5072
+ }
5073
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
5074
+ const logger = getLogger([
5075
+ "fedify",
5076
+ "federation",
5077
+ "inbox"
5078
+ ]);
5079
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
5080
+ const sends = inboxes.map((inbox) => sendActivity({
5081
+ activity,
5082
+ activityId: activityId.href,
5083
+ activityType,
5084
+ keys: [],
5085
+ inbox,
5086
+ allowPrivateAddress,
5087
+ meterProvider,
5088
+ tracerProvider
5089
+ }).catch((error) => {
5090
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5091
+ activityId: activityId.href,
5092
+ inbox: inbox.href,
5093
+ error
5094
+ });
5095
+ }));
5096
+ let timer;
5097
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
5098
+ timer = setTimeout(() => resolve(true), deadline);
5099
+ })]);
5100
+ clearTimeout(timer);
5101
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
5102
+ activityId: activityId.href,
5103
+ deadline
5104
+ });
5105
+ }
5106
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
5107
+ const logger = getLogger([
5108
+ "fedify",
5109
+ "federation",
5110
+ "inbox"
5111
+ ]);
5112
+ startQueue?.();
5113
+ const started = (/* @__PURE__ */ new Date()).toISOString();
5114
+ const traceContext = {};
5115
+ propagation.inject(context.active(), traceContext);
5116
+ await Promise.all(inboxes.map(async (inbox) => {
5117
+ const message = {
5118
+ type: "outbox",
5119
+ id: crypto.randomUUID(),
5120
+ baseUrl,
5121
+ keys: [],
5122
+ activity,
5123
+ activityId: activityId.href,
5124
+ activityType,
5125
+ inbox: inbox.href,
5126
+ sharedInbox: false,
5127
+ started,
5128
+ attempt: 0,
5129
+ headers: {},
5130
+ traceContext
5131
+ };
5132
+ try {
5133
+ await outboxQueue.enqueue(message);
5134
+ } catch (error) {
5135
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
5136
+ activityId: activityId.href,
5137
+ inbox: inbox.href,
5138
+ error
5139
+ });
5140
+ return;
5141
+ }
5142
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
5143
+ }));
5144
+ }
5145
+ //#endregion
4878
5146
  //#region src/federation/outgoing-proof.ts
4879
5147
  function isPortableUrl(url) {
4880
5148
  return url.protocol === "ap:" || url.protocol === "ap+ef61:";
@@ -5774,6 +6042,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5774
6042
  kvPrefixes;
5775
6043
  publicKeyTtl;
5776
6044
  httpMessageSignaturesSpecTtl;
6045
+ portableInboxForwarding;
5777
6046
  inboxQueue;
5778
6047
  outboxQueue;
5779
6048
  fanoutQueue;
@@ -5806,6 +6075,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5806
6075
  benchmarkMetricReader;
5807
6076
  benchmarkTriggerOptions;
5808
6077
  #mediaUploaderNoAuthWarned = false;
6078
+ #portableInboxForwardingWarned = false;
5809
6079
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
5810
6080
  #queueDepthGaugeEntries = [];
5811
6081
  #queueDepthGaugeMeterProvider;
@@ -5837,10 +6107,12 @@ var FederationImpl = class extends FederationBuilderImpl {
5837
6107
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
5838
6108
  circuitBreaker: ["_fedify", "circuit"],
5839
6109
  taskDeduplication: ["_fedify", "taskDeduplication"],
6110
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
5840
6111
  ...options.kvPrefixes ?? {}
5841
6112
  };
5842
6113
  this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
5843
6114
  this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
6115
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
5844
6116
  if (options.queue == null) {
5845
6117
  this.inboxQueue = void 0;
5846
6118
  this.outboxQueue = void 0;
@@ -6510,8 +6782,8 @@ var FederationImpl = class extends FederationBuilderImpl {
6510
6782
  ]);
6511
6783
  const baseUrl = new URL(message.baseUrl);
6512
6784
  let context = this.#createContext(baseUrl, ctxData);
6513
- if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6514
- else if (this.sharedInboxKeyDispatcher != null) {
6785
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
6786
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
6515
6787
  const identity = await this.sharedInboxKeyDispatcher(context);
6516
6788
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
6517
6789
  }
@@ -7104,6 +7376,15 @@ var FederationImpl = class extends FederationBuilderImpl {
7104
7376
  span,
7105
7377
  metricState
7106
7378
  });
7379
+ if (request.method === "POST") {
7380
+ const portable = parsePortableGatewayRequest(url);
7381
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
7382
+ onNotFound,
7383
+ contextData,
7384
+ span,
7385
+ metricState
7386
+ });
7387
+ }
7107
7388
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
7108
7389
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
7109
7390
  onNotFound,
@@ -7255,35 +7536,17 @@ var FederationImpl = class extends FederationBuilderImpl {
7255
7536
  onNotFound
7256
7537
  });
7257
7538
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
7258
- case "sharedInbox": {
7539
+ case "sharedInbox":
7259
7540
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
7260
7541
  const identity = await this.sharedInboxKeyDispatcher(context);
7261
7542
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
7262
7543
  }
7263
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7264
- const inboxContextFactory = context.toInboxContext.bind(context);
7265
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7266
- return await handleInbox(request, {
7544
+ return await this.#handleInbox(request, {
7267
7545
  recipient: route.values.identifier ?? null,
7268
7546
  context,
7269
- inboxContextFactory,
7270
- kv: this.kv,
7271
- kvPrefixes: this.kvPrefixes,
7272
- publicKeyTtl: this.publicKeyTtl,
7273
- queue: this.inboxQueue,
7274
- actorDispatcher: this.actorCallbacks?.dispatcher,
7275
- inboxListeners: this.inboxListeners,
7276
- inboxErrorHandler: this.inboxErrorHandler,
7277
- unverifiedActivityHandler: this.unverifiedActivityHandler,
7278
- onNotFound,
7279
- signatureTimeWindow: this.signatureTimeWindow,
7280
- skipSignatureVerification: this.skipSignatureVerification,
7281
- inboxChallengePolicy: this.inboxChallengePolicy,
7282
- meterProvider: this.meterProvider,
7283
- tracerProvider: this.tracerProvider,
7284
- idempotencyStrategy: this.idempotencyStrategy
7547
+ contextData,
7548
+ onNotFound
7285
7549
  });
7286
- }
7287
7550
  case "following": return await handleCollection(request, {
7288
7551
  name: "following",
7289
7552
  identifier: route.values.identifier,
@@ -7436,6 +7699,113 @@ var FederationImpl = class extends FederationBuilderImpl {
7436
7699
  onNotFound
7437
7700
  });
7438
7701
  }
7702
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
7703
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
7704
+ const inboxContextFactory = context.toInboxContext.bind(context);
7705
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
7706
+ return await handleInbox(request, {
7707
+ recipient,
7708
+ context,
7709
+ inboxContextFactory,
7710
+ kv: this.kv,
7711
+ kvPrefixes: this.kvPrefixes,
7712
+ publicKeyTtl: this.publicKeyTtl,
7713
+ queue: this.inboxQueue,
7714
+ actorDispatcher: this.actorCallbacks?.dispatcher,
7715
+ inboxListeners: this.inboxListeners,
7716
+ inboxErrorHandler: this.inboxErrorHandler,
7717
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
7718
+ onNotFound,
7719
+ signatureTimeWindow: this.signatureTimeWindow,
7720
+ skipSignatureVerification: this.skipSignatureVerification,
7721
+ inboxChallengePolicy: this.inboxChallengePolicy,
7722
+ meterProvider: this.meterProvider,
7723
+ tracerProvider: this.tracerProvider,
7724
+ idempotencyStrategy: this.idempotencyStrategy,
7725
+ portableInbox
7726
+ });
7727
+ }
7728
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
7729
+ const logger = getLogger([
7730
+ "fedify",
7731
+ "federation",
7732
+ "inbox"
7733
+ ]);
7734
+ if (portable.type === "malformed") {
7735
+ metricState.endpoint = "not_found";
7736
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
7737
+ url: request.url,
7738
+ error: portable.error
7739
+ });
7740
+ return new Response("Malformed portable inbox ID.", {
7741
+ status: 400,
7742
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
7743
+ });
7744
+ }
7745
+ const route = this.router.route(portable.path);
7746
+ if (route == null || route.name !== "inbox") {
7747
+ metricState.endpoint = "not_found";
7748
+ return await onNotFound(request);
7749
+ }
7750
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
7751
+ metricState.endpoint = "inbox";
7752
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
7753
+ const identifier = route.values.identifier;
7754
+ let context = this.#createContext(request, contextData);
7755
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
7756
+ const localOrigin = context.canonicalOrigin;
7757
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
7758
+ authority: portable.portableRequest.authority,
7759
+ canonicalInboxId: portable.canonicalId,
7760
+ localOrigin
7761
+ });
7762
+ if (resolution.status === "rejected") {
7763
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
7764
+ inbox: portable.canonicalId,
7765
+ identifier,
7766
+ reason: resolution.reason
7767
+ });
7768
+ return await onNotFound(request);
7769
+ }
7770
+ const { recipient } = resolution;
7771
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
7772
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
7773
+ return await this.#handleInbox(request, {
7774
+ recipient: identifier,
7775
+ context,
7776
+ contextData,
7777
+ onNotFound,
7778
+ portableInbox: {
7779
+ recipient,
7780
+ forward: async (activity, activityId, activityType) => {
7781
+ if (this.portableInboxForwarding.maxTargets < 1) return;
7782
+ if (this.kv.cas == null) {
7783
+ if (!this.#portableInboxForwardingWarned) {
7784
+ this.#portableInboxForwardingWarned = true;
7785
+ logger.warn("Activities delivered to FEP-ef61 portable inboxes are not forwarded to the other gateways, as the key–value store does not support compare-and-swap (KvStore.cas()), which is needed to forward each activity at most once.");
7786
+ }
7787
+ return;
7788
+ }
7789
+ await forwardPortableInboxActivity({
7790
+ recipient,
7791
+ activity,
7792
+ activityId,
7793
+ activityType,
7794
+ excludedOrigins,
7795
+ baseUrl: context.origin,
7796
+ kv: this.kv,
7797
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
7798
+ outboxQueue: this.outboxQueue,
7799
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
7800
+ allowPrivateAddress: this.allowPrivateAddress,
7801
+ options: this.portableInboxForwarding,
7802
+ meterProvider: this.meterProvider,
7803
+ tracerProvider: this.tracerProvider
7804
+ });
7805
+ }
7806
+ }
7807
+ });
7808
+ }
7439
7809
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
7440
7810
  metricState.endpoint = "hashlink_media";
7441
7811
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -7666,6 +8036,12 @@ var ContextImpl = class ContextImpl {
7666
8036
  if (path == null) throw new RouterError("No inbox path registered.");
7667
8037
  return new URL(path, this.canonicalOrigin);
7668
8038
  }
8039
+ getPortableInboxUri(identifier, authority) {
8040
+ const path = this.federation.router.build("inbox", { identifier });
8041
+ if (path == null) throw new RouterError("No inbox path registered.");
8042
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
8043
+ return buildPortableUri(authority, path);
8044
+ }
7669
8045
  getFollowingUri(identifier) {
7670
8046
  const path = this.federation.router.build("following", { identifier });
7671
8047
  if (path == null) throw new RouterError("No following collection path registered.");
@@ -8268,6 +8644,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
8268
8644
  getPortableObjectUri(cls, values, authority) {
8269
8645
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
8270
8646
  }
8647
+ getPortableInboxUri(identifier, authority) {
8648
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
8649
+ }
8271
8650
  async getActor(identifier, options) {
8272
8651
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
8273
8652
  if (this.#invokedFromActorDispatcher != null) getLogger([