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

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-i3jxVe9m.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-Cayy-ECj.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-Djkzd-G_.mjs} +1 -1
  10. package/dist/{docloader-C6yOVoaF.mjs → docloader-dA0RswUV.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 +138 -11
  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-CFd0Jhhv.cjs} +463 -82
  34. package/dist/{http-DKWi8RjI.mjs → http-CCJ97Jql.mjs} +4 -4
  35. package/dist/{http-CuLWIJdf.cjs → http-Dvy_IwRa.cjs} +1 -1
  36. package/dist/{http-BoWBwC0g.js → http-aqh3yFdM.js} +1 -1
  37. package/dist/{key-lfcx_NaE.mjs → key-IxTTC8Yf.mjs} +2 -2
  38. package/dist/{kv-cache-DE8kSNQU.cjs → kv-cache-BP69NV0P.cjs} +1 -1
  39. package/dist/{kv-cache-d8EjO29d.mjs → kv-cache-Bwwbh-xR.mjs} +1 -1
  40. package/dist/{kv-cache-BdnmnAVc.js → kv-cache-Y6SDRSX6.js} +1 -1
  41. package/dist/{ld-jyjdrWjG.mjs → ld-l0B9nREb.mjs} +3 -3
  42. package/dist/{metrics-B5Rei-HC.mjs → metrics-D1p6hv4z.mjs} +1 -1
  43. package/dist/{middleware-CdVnCDXD.mjs → middleware-BDjCUHMS.mjs} +1 -1
  44. package/dist/{middleware-CPx4ZWwt.mjs → middleware-BMgzqBtr.mjs} +418 -42
  45. package/dist/{middleware-CAObohey.js → middleware-SSfryVzY.js} +412 -32
  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-BXufwAzm.cjs → owner-BpOxL5PG.cjs} +1 -1
  54. package/dist/{owner-ByqNQVaa.mjs → owner-DGbYmXqU.mjs} +3 -3
  55. package/dist/{owner-1TFakcTF.js → owner-jH1z_YfD.js} +1 -1
  56. package/dist/{portable-key-Cw_ow5vn.mjs → portable-key-CCwJGSsU.mjs} +2 -2
  57. package/dist/{proof-B6gwdhN5.mjs → proof-CVL7JnZd.mjs} +4 -4
  58. package/dist/{send-DsK7xevO.mjs → send-Ejy1jOz9.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-BR_Z6mpY.js → sig-Dpy1B1Ad.js} +2 -2
  73. package/dist/{sig-TTn9wYrE.cjs → sig-xoqy3ATq.cjs} +2 -2
  74. package/dist/{temporal-Bw5jtrBY.mjs → temporal-DNJ1cNpr.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 +7 -7
@@ -1,27 +1,27 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { n as version, t as name } from "./deno-B-NtYZVW.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-B5Rei-HC.mjs";
4
+ import { n as version, t as name } from "./deno-Djkzd-G_.mjs";
5
+ import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-D1p6hv4z.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { d as hasPortableActor, f as isCompatibleKeyId, m as isPortableUri, o as importJwk, s as validateCryptoKey, t as exportJwk, u as getGatewayKeyBase } from "./key-lfcx_NaE.mjs";
7
+ import { d as hasPortableActor, f as isCompatibleKeyId, m as isPortableUri, o as importJwk, s as validateCryptoKey, t as exportJwk, u as getGatewayKeyBase } from "./key-IxTTC8Yf.mjs";
8
8
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
9
- import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-jyjdrWjG.mjs";
10
- import { a as verifyObject, n as hasProofLike, o as verifyPortableObjectProof, r as signObject } from "./proof-B6gwdhN5.mjs";
11
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-DKWi8RjI.mjs";
12
- import { t as getAuthenticatedDocumentLoader } from "./docloader-C6yOVoaF.mjs";
13
- import { n as kvCache } from "./kv-cache-d8EjO29d.mjs";
9
+ import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-l0B9nREb.mjs";
10
+ import { a as verifyObject, n as hasProofLike, o as verifyPortableObjectProof, r as signObject } from "./proof-CVL7JnZd.mjs";
11
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-CCJ97Jql.mjs";
12
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-dA0RswUV.mjs";
13
+ import { n as kvCache } from "./kv-cache-Bwwbh-xR.mjs";
14
14
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DpnpgjyX.mjs";
15
15
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
16
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-ByqNQVaa.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-DsK7xevO.mjs";
18
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-DVQ0yhKJ.mjs";
16
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DGbYmXqU.mjs";
17
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-Ejy1jOz9.mjs";
18
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-i3jxVe9m.mjs";
19
19
  import { t as CircuitBreaker } from "./circuit-breaker-C5NqoLBJ.mjs";
20
20
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
21
- import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-KmXDfWFp.mjs";
21
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-Cayy-ECj.mjs";
22
22
  import { t as KvKeyCache } from "./keycache-BUDHB5rY.mjs";
23
23
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
24
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Bw5jtrBY.mjs";
24
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-DNJ1cNpr.mjs";
25
25
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
26
26
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
27
27
  import { getLogger, withContext } from "@logtape/logtape";
@@ -1467,7 +1467,7 @@ async function handleInbox(request, options) {
1467
1467
  * @returns A promise that resolves to an HTTP response.
1468
1468
  */
1469
1469
  async function handleInboxInternal(request, parameters, span) {
1470
- const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
1470
+ const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
1471
1471
  const logger = getLogger([
1472
1472
  "fedify",
1473
1473
  "federation",
@@ -1480,7 +1480,7 @@ async function handleInboxInternal(request, parameters, span) {
1480
1480
  message: "Actor dispatcher is not set."
1481
1481
  });
1482
1482
  return await onNotFound(request);
1483
- } else if (recipient != null) {
1483
+ } else if (recipient != null && portableInbox == null) {
1484
1484
  const actor = await actorDispatcher(ctx, recipient);
1485
1485
  if (actor == null || actor instanceof Tombstone) {
1486
1486
  logger.error("Actor {recipient} not found.", { recipient });
@@ -1891,8 +1891,9 @@ async function handleInboxInternal(request, parameters, span) {
1891
1891
  });
1892
1892
  }
1893
1893
  if (!skipSignatureVerification) {
1894
- if (inspectCompoundPortableObjectApplicability(json, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
1895
- const compoundProof = await verifyCompoundPortableObjectProofs(json, INBOX_COMPOUND_PROOF_LIMITS, {
1894
+ const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
1895
+ if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
1896
+ const compoundProof = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
1896
1897
  documentLoader: ctx.documentLoader,
1897
1898
  keyCache,
1898
1899
  meterProvider,
@@ -1942,6 +1943,19 @@ async function handleInboxInternal(request, parameters, span) {
1942
1943
  tracerProvider,
1943
1944
  idempotencyStrategy: parameters.idempotencyStrategy
1944
1945
  });
1946
+ 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.", {
1947
+ activityId: activity.id.href,
1948
+ inbox: portableInbox.recipient.canonicalInboxId
1949
+ });
1950
+ else try {
1951
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href);
1952
+ } catch (error) {
1953
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
1954
+ activityId: activity.id.href,
1955
+ inbox: portableInbox.recipient.canonicalInboxId,
1956
+ error
1957
+ });
1958
+ }
1945
1959
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
1946
1960
  status: 202,
1947
1961
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -1967,6 +1981,12 @@ async function handleInboxInternal(request, parameters, span) {
1967
1981
  headers: { "Content-Type": "text/plain; charset=utf-8" }
1968
1982
  });
1969
1983
  }
1984
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
1985
+ "success",
1986
+ "enqueued",
1987
+ "alreadyProcessed",
1988
+ "unsupportedActivity"
1989
+ ]);
1970
1990
  /**
1971
1991
  * Handles a custom collection request.
1972
1992
  * @template TItem The type of items in the collection.
@@ -2584,6 +2604,251 @@ function isPortableGatewayKey(key) {
2584
2604
  return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableUri(key.ownerId);
2585
2605
  }
2586
2606
  //#endregion
2607
+ //#region src/federation/portable-inbox.ts
2608
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2609
+ /**
2610
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2611
+ * validates them.
2612
+ * @param options The options.
2613
+ * @returns The resolved options.
2614
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2615
+ * is not positive, if `deadline` is negative or longer
2616
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2617
+ * if either duration has calendar units, i.e., weeks,
2618
+ * months, or years, whose length depends on the date.
2619
+ */
2620
+ function resolvePortableInboxForwardingOptions(options = {}) {
2621
+ const maxTargets = options.maxTargets ?? 10;
2622
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2623
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
2624
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2625
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
2626
+ const deadlineMs = deadline.total("millisecond");
2627
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2628
+ return {
2629
+ maxTargets,
2630
+ ttl,
2631
+ deadline
2632
+ };
2633
+ }
2634
+ /**
2635
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2636
+ * of the given actor, which the actor dispatcher returned for the identifier
2637
+ * in the requested inbox path.
2638
+ *
2639
+ * The actor document comes from the application, so it is trusted as is and
2640
+ * its proof is not verified here.
2641
+ * @param actor The actor the actor dispatcher returned.
2642
+ * @param options The requested DID, the canonical form of the requested inbox
2643
+ * ID, and the origin of this server.
2644
+ * @returns The resolution.
2645
+ */
2646
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2647
+ if (actor == null) return {
2648
+ status: "rejected",
2649
+ reason: "notFound"
2650
+ };
2651
+ const id = actor.id;
2652
+ if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
2653
+ status: "rejected",
2654
+ reason: "notPortable"
2655
+ };
2656
+ if (!isSameAuthority(id, authority)) return {
2657
+ status: "rejected",
2658
+ reason: "authorityMismatch"
2659
+ };
2660
+ const inboxId = actor.inboxId;
2661
+ if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
2662
+ status: "rejected",
2663
+ reason: "inboxMismatch"
2664
+ };
2665
+ const gateways = actor.gateways;
2666
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
2667
+ status: "rejected",
2668
+ reason: "notGateway"
2669
+ };
2670
+ return {
2671
+ status: "accepted",
2672
+ recipient: {
2673
+ inboxId,
2674
+ canonicalInboxId,
2675
+ gateways
2676
+ }
2677
+ };
2678
+ }
2679
+ function isSameAuthority(id, authority) {
2680
+ try {
2681
+ return getFe34Origin(id) === authority;
2682
+ } catch (error) {
2683
+ if (error instanceof TypeError) return false;
2684
+ throw error;
2685
+ }
2686
+ }
2687
+ function canonicalize(iri) {
2688
+ try {
2689
+ return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
2690
+ } catch (error) {
2691
+ if (error instanceof TypeError) return null;
2692
+ throw error;
2693
+ }
2694
+ }
2695
+ /**
2696
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2697
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2698
+ *
2699
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2700
+ * each target gateway is claimed with an atomic compare-and-swap before the
2701
+ * activity is handed off, and a claim is never released, even if the hand-off
2702
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2703
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2704
+ * forwarded.
2705
+ *
2706
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
2707
+ * gateways authenticate the activity by its own proof.
2708
+ * @param parameters The parameters.
2709
+ * @returns The target inbox URLs that the activity was handed off to.
2710
+ */
2711
+ async function forwardPortableInboxActivity(parameters) {
2712
+ const logger = getLogger([
2713
+ "fedify",
2714
+ "federation",
2715
+ "inbox"
2716
+ ]);
2717
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2718
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
2719
+ if (kv.cas == null || maxTargets < 1) return [];
2720
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
2721
+ const targets = getForwardingTargets(recipient, excludedOrigins);
2722
+ if (targets.length > maxTargets) {
2723
+ 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.", {
2724
+ inbox: recipient.canonicalInboxId,
2725
+ activityId: activityId.href,
2726
+ max: maxTargets
2727
+ });
2728
+ targets.length = maxTargets;
2729
+ }
2730
+ const claimed = [];
2731
+ for (const { gateway, inbox } of targets) {
2732
+ const key = [
2733
+ ...kvPrefix,
2734
+ recipient.canonicalInboxId,
2735
+ canonicalActivityId,
2736
+ gateway
2737
+ ];
2738
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
2739
+ }
2740
+ if (claimed.length < 1) return [];
2741
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
2742
+ activityId: activityId.href,
2743
+ inbox: recipient.canonicalInboxId,
2744
+ targets: claimed.map((t) => t.href)
2745
+ });
2746
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
2747
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
2748
+ return claimed;
2749
+ }
2750
+ function getForwardingTargets(recipient, excludedOrigins) {
2751
+ const logger = getLogger([
2752
+ "fedify",
2753
+ "federation",
2754
+ "inbox"
2755
+ ]);
2756
+ const seen = new Set(excludedOrigins);
2757
+ const targets = [];
2758
+ for (const gateway of recipient.gateways) {
2759
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
2760
+ seen.add(gateway.origin);
2761
+ let inbox;
2762
+ try {
2763
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
2764
+ } catch (error) {
2765
+ if (!(error instanceof TypeError)) throw error;
2766
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
2767
+ gateway: gateway.origin,
2768
+ inbox: recipient.canonicalInboxId
2769
+ });
2770
+ continue;
2771
+ }
2772
+ targets.push({
2773
+ gateway: gateway.origin,
2774
+ inbox
2775
+ });
2776
+ }
2777
+ return targets;
2778
+ }
2779
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
2780
+ const logger = getLogger([
2781
+ "fedify",
2782
+ "federation",
2783
+ "inbox"
2784
+ ]);
2785
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
2786
+ const sends = inboxes.map((inbox) => sendActivity({
2787
+ activity,
2788
+ activityId: activityId.href,
2789
+ activityType,
2790
+ keys: [],
2791
+ inbox,
2792
+ allowPrivateAddress,
2793
+ meterProvider,
2794
+ tracerProvider
2795
+ }).catch((error) => {
2796
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2797
+ activityId: activityId.href,
2798
+ inbox: inbox.href,
2799
+ error
2800
+ });
2801
+ }));
2802
+ let timer;
2803
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
2804
+ timer = setTimeout(() => resolve(true), deadline);
2805
+ })]);
2806
+ clearTimeout(timer);
2807
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
2808
+ activityId: activityId.href,
2809
+ deadline
2810
+ });
2811
+ }
2812
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
2813
+ const logger = getLogger([
2814
+ "fedify",
2815
+ "federation",
2816
+ "inbox"
2817
+ ]);
2818
+ startQueue?.();
2819
+ const started = (/* @__PURE__ */ new Date()).toISOString();
2820
+ const traceContext = {};
2821
+ propagation.inject(context.active(), traceContext);
2822
+ await Promise.all(inboxes.map(async (inbox) => {
2823
+ const message = {
2824
+ type: "outbox",
2825
+ id: crypto.randomUUID(),
2826
+ baseUrl,
2827
+ keys: [],
2828
+ activity,
2829
+ activityId: activityId.href,
2830
+ activityType,
2831
+ inbox: inbox.href,
2832
+ sharedInbox: false,
2833
+ started,
2834
+ attempt: 0,
2835
+ headers: {},
2836
+ traceContext
2837
+ };
2838
+ try {
2839
+ await outboxQueue.enqueue(message);
2840
+ } catch (error) {
2841
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2842
+ activityId: activityId.href,
2843
+ inbox: inbox.href,
2844
+ error
2845
+ });
2846
+ return;
2847
+ }
2848
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
2849
+ }));
2850
+ }
2851
+ //#endregion
2587
2852
  //#region src/federation/outgoing-proof.ts
2588
2853
  function isPortableUrl(url) {
2589
2854
  return url.protocol === "ap:" || url.protocol === "ap+ef61:";
@@ -3304,6 +3569,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3304
3569
  kvPrefixes;
3305
3570
  publicKeyTtl;
3306
3571
  httpMessageSignaturesSpecTtl;
3572
+ portableInboxForwarding;
3307
3573
  inboxQueue;
3308
3574
  outboxQueue;
3309
3575
  fanoutQueue;
@@ -3336,6 +3602,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3336
3602
  benchmarkMetricReader;
3337
3603
  benchmarkTriggerOptions;
3338
3604
  #mediaUploaderNoAuthWarned = false;
3605
+ #portableInboxForwardingWarned = false;
3339
3606
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
3340
3607
  #queueDepthGaugeEntries = [];
3341
3608
  #queueDepthGaugeMeterProvider;
@@ -3367,10 +3634,12 @@ var FederationImpl = class extends FederationBuilderImpl {
3367
3634
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
3368
3635
  circuitBreaker: ["_fedify", "circuit"],
3369
3636
  taskDeduplication: ["_fedify", "taskDeduplication"],
3637
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
3370
3638
  ...options.kvPrefixes ?? {}
3371
3639
  };
3372
3640
  this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
3373
3641
  this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
3642
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
3374
3643
  if (options.queue == null) {
3375
3644
  this.inboxQueue = void 0;
3376
3645
  this.outboxQueue = void 0;
@@ -4040,8 +4309,8 @@ var FederationImpl = class extends FederationBuilderImpl {
4040
4309
  ]);
4041
4310
  const baseUrl = new URL(message.baseUrl);
4042
4311
  let context = this.#createContext(baseUrl, ctxData);
4043
- if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
4044
- else if (this.sharedInboxKeyDispatcher != null) {
4312
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
4313
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
4045
4314
  const identity = await this.sharedInboxKeyDispatcher(context);
4046
4315
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
4047
4316
  }
@@ -4634,6 +4903,15 @@ var FederationImpl = class extends FederationBuilderImpl {
4634
4903
  span,
4635
4904
  metricState
4636
4905
  });
4906
+ if (request.method === "POST") {
4907
+ const portable = parsePortableGatewayRequest(url);
4908
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
4909
+ onNotFound,
4910
+ contextData,
4911
+ span,
4912
+ metricState
4913
+ });
4914
+ }
4637
4915
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
4638
4916
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
4639
4917
  onNotFound,
@@ -4785,35 +5063,17 @@ var FederationImpl = class extends FederationBuilderImpl {
4785
5063
  onNotFound
4786
5064
  });
4787
5065
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
4788
- case "sharedInbox": {
5066
+ case "sharedInbox":
4789
5067
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
4790
5068
  const identity = await this.sharedInboxKeyDispatcher(context);
4791
5069
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
4792
5070
  }
4793
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
4794
- const inboxContextFactory = context.toInboxContext.bind(context);
4795
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
4796
- return await handleInbox(request, {
5071
+ return await this.#handleInbox(request, {
4797
5072
  recipient: route.values.identifier ?? null,
4798
5073
  context,
4799
- inboxContextFactory,
4800
- kv: this.kv,
4801
- kvPrefixes: this.kvPrefixes,
4802
- publicKeyTtl: this.publicKeyTtl,
4803
- queue: this.inboxQueue,
4804
- actorDispatcher: this.actorCallbacks?.dispatcher,
4805
- inboxListeners: this.inboxListeners,
4806
- inboxErrorHandler: this.inboxErrorHandler,
4807
- unverifiedActivityHandler: this.unverifiedActivityHandler,
4808
- onNotFound,
4809
- signatureTimeWindow: this.signatureTimeWindow,
4810
- skipSignatureVerification: this.skipSignatureVerification,
4811
- inboxChallengePolicy: this.inboxChallengePolicy,
4812
- meterProvider: this.meterProvider,
4813
- tracerProvider: this.tracerProvider,
4814
- idempotencyStrategy: this.idempotencyStrategy
5074
+ contextData,
5075
+ onNotFound
4815
5076
  });
4816
- }
4817
5077
  case "following": return await handleCollection(request, {
4818
5078
  name: "following",
4819
5079
  identifier: route.values.identifier,
@@ -4966,6 +5226,113 @@ var FederationImpl = class extends FederationBuilderImpl {
4966
5226
  onNotFound
4967
5227
  });
4968
5228
  }
5229
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
5230
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
5231
+ const inboxContextFactory = context.toInboxContext.bind(context);
5232
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
5233
+ return await handleInbox(request, {
5234
+ recipient,
5235
+ context,
5236
+ inboxContextFactory,
5237
+ kv: this.kv,
5238
+ kvPrefixes: this.kvPrefixes,
5239
+ publicKeyTtl: this.publicKeyTtl,
5240
+ queue: this.inboxQueue,
5241
+ actorDispatcher: this.actorCallbacks?.dispatcher,
5242
+ inboxListeners: this.inboxListeners,
5243
+ inboxErrorHandler: this.inboxErrorHandler,
5244
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
5245
+ onNotFound,
5246
+ signatureTimeWindow: this.signatureTimeWindow,
5247
+ skipSignatureVerification: this.skipSignatureVerification,
5248
+ inboxChallengePolicy: this.inboxChallengePolicy,
5249
+ meterProvider: this.meterProvider,
5250
+ tracerProvider: this.tracerProvider,
5251
+ idempotencyStrategy: this.idempotencyStrategy,
5252
+ portableInbox
5253
+ });
5254
+ }
5255
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
5256
+ const logger = getLogger([
5257
+ "fedify",
5258
+ "federation",
5259
+ "inbox"
5260
+ ]);
5261
+ if (portable.type === "malformed") {
5262
+ metricState.endpoint = "not_found";
5263
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
5264
+ url: request.url,
5265
+ error: portable.error
5266
+ });
5267
+ return new Response("Malformed portable inbox ID.", {
5268
+ status: 400,
5269
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
5270
+ });
5271
+ }
5272
+ const route = this.router.route(portable.path);
5273
+ if (route == null || route.name !== "inbox") {
5274
+ metricState.endpoint = "not_found";
5275
+ return await onNotFound(request);
5276
+ }
5277
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
5278
+ metricState.endpoint = "inbox";
5279
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
5280
+ const identifier = route.values.identifier;
5281
+ let context = this.#createContext(request, contextData);
5282
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
5283
+ const localOrigin = context.canonicalOrigin;
5284
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
5285
+ authority: portable.portableRequest.authority,
5286
+ canonicalInboxId: portable.canonicalId,
5287
+ localOrigin
5288
+ });
5289
+ if (resolution.status === "rejected") {
5290
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
5291
+ inbox: portable.canonicalId,
5292
+ identifier,
5293
+ reason: resolution.reason
5294
+ });
5295
+ return await onNotFound(request);
5296
+ }
5297
+ const { recipient } = resolution;
5298
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
5299
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
5300
+ return await this.#handleInbox(request, {
5301
+ recipient: identifier,
5302
+ context,
5303
+ contextData,
5304
+ onNotFound,
5305
+ portableInbox: {
5306
+ recipient,
5307
+ forward: async (activity, activityId, activityType) => {
5308
+ if (this.portableInboxForwarding.maxTargets < 1) return;
5309
+ if (this.kv.cas == null) {
5310
+ if (!this.#portableInboxForwardingWarned) {
5311
+ this.#portableInboxForwardingWarned = true;
5312
+ 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.");
5313
+ }
5314
+ return;
5315
+ }
5316
+ await forwardPortableInboxActivity({
5317
+ recipient,
5318
+ activity,
5319
+ activityId,
5320
+ activityType,
5321
+ excludedOrigins,
5322
+ baseUrl: context.origin,
5323
+ kv: this.kv,
5324
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
5325
+ outboxQueue: this.outboxQueue,
5326
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
5327
+ allowPrivateAddress: this.allowPrivateAddress,
5328
+ options: this.portableInboxForwarding,
5329
+ meterProvider: this.meterProvider,
5330
+ tracerProvider: this.tracerProvider
5331
+ });
5332
+ }
5333
+ }
5334
+ });
5335
+ }
4969
5336
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
4970
5337
  metricState.endpoint = "hashlink_media";
4971
5338
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -5196,6 +5563,12 @@ var ContextImpl = class ContextImpl {
5196
5563
  if (path == null) throw new RouterError("No inbox path registered.");
5197
5564
  return new URL(path, this.canonicalOrigin);
5198
5565
  }
5566
+ getPortableInboxUri(identifier, authority) {
5567
+ const path = this.federation.router.build("inbox", { identifier });
5568
+ if (path == null) throw new RouterError("No inbox path registered.");
5569
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5570
+ return buildPortableUri(authority, path);
5571
+ }
5199
5572
  getFollowingUri(identifier) {
5200
5573
  const path = this.federation.router.build("following", { identifier });
5201
5574
  if (path == null) throw new RouterError("No following collection path registered.");
@@ -5798,6 +6171,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
5798
6171
  getPortableObjectUri(cls, values, authority) {
5799
6172
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
5800
6173
  }
6174
+ getPortableInboxUri(identifier, authority) {
6175
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
6176
+ }
5801
6177
  async getActor(identifier, options) {
5802
6178
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
5803
6179
  if (this.#invokedFromActorDispatcher != null) getLogger([
@@ -6188,4 +6564,4 @@ function getRequestId(request) {
6188
6564
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
6189
6565
  }
6190
6566
  //#endregion
6191
- export { autoIdAssigner as S, parseHashlinkGatewayRequest as _, OutboxContextImpl as a, handleNodeInfoJrd as b, handleActor as c, handleInbox as d, handleObject as f, PORTABLE_OBJECT_CONTENT_TYPE as g, respondWithObjectIfAcceptable as h, KvSpecDeterminer as i, handleCollection as l, respondWithObject as m, FederationImpl as n, createFederation as o, handleOutbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCustomCollection as u, handleBenchmarkTrigger as v, actorDehydrator as x, handleNodeInfo as y };
6567
+ export { actorDehydrator as C, handleNodeInfoJrd as S, respondWithObjectIfAcceptable as _, OutboxContextImpl as a, handleBenchmarkTrigger as b, forwardPortableInboxActivity as c, handleCollection as d, handleCustomCollection as f, respondWithObject as g, handleOutbox as h, KvSpecDeterminer as i, resolvePortableInboxForwardingOptions as l, handleObject as m, FederationImpl as n, createFederation as o, handleInbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleActor as u, PORTABLE_OBJECT_CONTENT_TYPE as v, autoIdAssigner as w, handleNodeInfo as x, parseHashlinkGatewayRequest as y };