@fedify/fedify 2.4.0-dev.2065 → 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 (98) hide show
  1. package/dist/{builder-CMkrnYbY.mjs → builder-CVHpPAjK.mjs} +6 -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-Dj3DCsoj.mjs → compound-proof-B5Skp-vE.mjs} +1 -1
  6. package/dist/{context-D_gg4M0n.d.ts → context-BSywYspi.d.ts} +153 -2
  7. package/dist/{context-b36TpPoG.d.cts → context-CZjZYXFC.d.cts} +153 -2
  8. package/dist/{context-DG5QeQ8W.mjs → context-DlZhxdFT.mjs} +2 -1
  9. package/dist/{deno-BuRFI7wk.mjs → deno-LYTbBhYV.mjs} +1 -1
  10. package/dist/{docloader-_aSNLA90.mjs → docloader-C0SWQylk.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +4 -4
  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 +9 -9
  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 +6 -6
  24. package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
  25. package/dist/federation/portable-gateway-keys.test.mjs +363 -0
  26. package/dist/federation/portable-inbox.test.d.mts +2 -0
  27. package/dist/federation/portable-inbox.test.mjs +546 -0
  28. package/dist/federation/portable.test.mjs +3 -3
  29. package/dist/federation/send.test.mjs +3 -3
  30. package/dist/federation/tasks/enqueue.test.mjs +2 -2
  31. package/dist/federation/tasks/tasks.test.mjs +3 -3
  32. package/dist/federation/temporal.test.mjs +1 -1
  33. package/dist/federation/webfinger.test.mjs +2 -2
  34. package/dist/{federation-Coi_bg12.cjs → federation-B1aeTKKN.cjs} +631 -145
  35. package/dist/{http-f1qaBtOh.mjs → http-C7qHlv-b.mjs} +20 -5
  36. package/dist/{http-BGShIsd6.cjs → http-DGk8XI38.cjs} +2684 -48
  37. package/dist/{http-65PYi9jY.d.ts → http-n2v6DPxu.d.ts} +52 -2
  38. package/dist/http-vfTpzGEq.js +5087 -0
  39. package/dist/{http-CP-7OdSq.d.cts → http-xlG-jRdB.d.cts} +52 -2
  40. package/dist/{key-BxHsA1s9.mjs → key-BYXnTGro.mjs} +147 -7
  41. package/dist/{kv-cache-Cgj-kpQS.js → kv-cache-DCJuwdnB.js} +1 -1
  42. package/dist/{kv-cache-C9IY-FMN.mjs → kv-cache-DEH5jyND.mjs} +1 -1
  43. package/dist/{kv-cache-CY6pDNoS.cjs → kv-cache-DvqkKNIj.cjs} +1 -1
  44. package/dist/{ld-CqJq5erX.mjs → ld-DT3e2hOv.mjs} +3 -3
  45. package/dist/{metrics-DOqknlZd.mjs → metrics-DvlvYpq_.mjs} +1 -1
  46. package/dist/{middleware-VOCZN3SO.mjs → middleware-BJcez_E3.mjs} +1 -1
  47. package/dist/{middleware-1h_LgwXC.js → middleware-CCbyUF4c.js} +547 -62
  48. package/dist/{middleware-s-AZzx4r.mjs → middleware-CpyCNOW_.mjs} +549 -72
  49. package/dist/{mod-BhMvsgdF.d.ts → mod-B2h2KY0q.d.ts} +1 -1
  50. package/dist/{mod-Dhm7SqyE.d.ts → mod-C9WSpZWm.d.ts} +1 -1
  51. package/dist/{mod-C0BYc0OU.d.ts → mod-CVZrXSdm.d.ts} +1 -1
  52. package/dist/{mod-pLbeNwcC.d.cts → mod-HDSvS-ww.d.cts} +1 -1
  53. package/dist/{mod-Bfenkg_c.d.cts → mod-JzkaEXsi.d.cts} +1 -1
  54. package/dist/{mod-CyiSjRkk.d.cts → mod-xGFCHqq_.d.cts} +1 -1
  55. package/dist/mod.cjs +18 -18
  56. package/dist/mod.d.cts +7 -7
  57. package/dist/mod.d.ts +7 -7
  58. package/dist/mod.js +5 -5
  59. package/dist/nodeinfo/handler.test.mjs +2 -2
  60. package/dist/{owner-D_F22lfM.mjs → owner-CDsVeZI7.mjs} +45 -2
  61. package/dist/owner-DCIH7Iq0.cjs +225 -0
  62. package/dist/owner-DfGz0cXm.js +215 -0
  63. package/dist/portable-key-DCyKGCQd.mjs +194 -0
  64. package/dist/{proof-B03JXhrk.mjs → proof-CxsVG6vM.mjs} +5 -5
  65. package/dist/{send-NNkfKtVN.mjs → send-BZJ7NsVV.mjs} +8 -4
  66. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  67. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  68. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  69. package/dist/sig/compound-proof.test.mjs +3 -3
  70. package/dist/sig/http.test.mjs +2 -2
  71. package/dist/sig/key.test.mjs +1 -1
  72. package/dist/sig/ld.test.mjs +2 -2
  73. package/dist/sig/mod.cjs +16 -16
  74. package/dist/sig/mod.d.cts +2 -2
  75. package/dist/sig/mod.d.ts +2 -2
  76. package/dist/sig/mod.js +3 -3
  77. package/dist/sig/owner.test.mjs +1 -1
  78. package/dist/sig/portable-collection.test.mjs +3 -3
  79. package/dist/sig/portable-key.test.d.mts +2 -0
  80. package/dist/sig/portable-key.test.mjs +275 -0
  81. package/dist/sig/proof.test.mjs +4 -4
  82. package/dist/{sig-B1E2uyhQ.cjs → sig-C6TnADze.cjs} +4 -4
  83. package/dist/{sig-Bb20Pyrd.js → sig-Cs9_ecdh.js} +2 -2
  84. package/dist/{temporal-VVVSuruq.mjs → temporal-CLzSfIOV.mjs} +1 -1
  85. package/dist/testing/kv.test.mjs +1 -1
  86. package/dist/testing/mod.d.mts +151 -0
  87. package/dist/testing/mod.mjs +2 -2
  88. package/dist/{testing-By1YYsIK.mjs → testing-CO_cEvQc.mjs} +1 -1
  89. package/dist/utils/docloader.test.mjs +2 -2
  90. package/dist/utils/kv-cache.test.mjs +1 -1
  91. package/dist/utils/mod.cjs +1 -1
  92. package/dist/utils/mod.d.cts +1 -1
  93. package/dist/utils/mod.d.ts +1 -1
  94. package/dist/utils/mod.js +1 -1
  95. package/package.json +6 -6
  96. package/dist/http-z5n7chWw.js +0 -2647
  97. package/dist/owner-BLBWXCNt.js +0 -2276
  98. package/dist/owner-D8ZpRusc.cjs +0 -2446
@@ -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-BuRFI7wk.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-DOqknlZd.mjs";
4
+ import { n as version, t as name } from "./deno-LYTbBhYV.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-DvlvYpq_.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
7
- import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-BxHsA1s9.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-f1qaBtOh.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-_aSNLA90.mjs";
10
- import { n as kvCache } from "./kv-cache-C9IY-FMN.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-BYXnTGro.mjs";
11
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-DT3e2hOv.mjs";
10
+ import { a as verifyObject, n as hasProofLike, o as verifyPortableObjectProof, r as signObject } from "./proof-CxsVG6vM.mjs";
11
+ import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-C7qHlv-b.mjs";
12
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-C0SWQylk.mjs";
13
+ import { n as kvCache } from "./kv-cache-DEH5jyND.mjs";
12
14
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DpnpgjyX.mjs";
13
15
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
14
- 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-CqJq5erX.mjs";
15
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-D_F22lfM.mjs";
16
- import { a as verifyObject, n as hasProofLike, o as verifyPortableObjectProof, r as signObject } from "./proof-B03JXhrk.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-NNkfKtVN.mjs";
18
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CMkrnYbY.mjs";
16
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-CDsVeZI7.mjs";
17
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BZJ7NsVV.mjs";
18
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CVHpPAjK.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-Dj3DCsoj.mjs";
21
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-B5Skp-vE.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-VVVSuruq.mjs";
24
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CLzSfIOV.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 });
@@ -1653,6 +1653,7 @@ async function handleInboxInternal(request, parameters, span) {
1653
1653
  }
1654
1654
  let activity = null;
1655
1655
  let activityVerified = false;
1656
+ let proofVerified = false;
1656
1657
  if (ldSigVerified) {
1657
1658
  logger.debug("Linked Data Signatures are verified.", {
1658
1659
  recipient,
@@ -1669,6 +1670,21 @@ async function handleInboxInternal(request, parameters, span) {
1669
1670
  return await respondInvalidActivity(error);
1670
1671
  }
1671
1672
  activityVerified = true;
1673
+ if (!skipSignatureVerification && hasPortableActor(activity)) try {
1674
+ proofVerified = await verifyObject(Activity, jsonWithoutSig, {
1675
+ contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
1676
+ documentLoader: ctx.documentLoader,
1677
+ keyCache,
1678
+ meterProvider,
1679
+ tracerProvider
1680
+ }) != null;
1681
+ } catch (error) {
1682
+ if (!isPermanentActivityParseError(error)) throw error;
1683
+ logger.debug("Failed to verify the Object Integrity Proofs of the portable actor's activity:\n{error}", {
1684
+ recipient,
1685
+ error
1686
+ });
1687
+ }
1672
1688
  } else {
1673
1689
  logger.debug("Linked Data Signatures are not verified.", {
1674
1690
  recipient,
@@ -1725,6 +1741,7 @@ async function handleInboxInternal(request, parameters, span) {
1725
1741
  activity: json
1726
1742
  });
1727
1743
  activityVerified = true;
1744
+ proofVerified = true;
1728
1745
  }
1729
1746
  }
1730
1747
  let httpSigKey = null;
@@ -1837,6 +1854,24 @@ async function handleInboxInternal(request, parameters, span) {
1837
1854
  "http_signatures.verified": httpSigKey != null,
1838
1855
  "http_signatures.key_id": httpSigKey?.id?.href ?? ""
1839
1856
  });
1857
+ if (!skipSignatureVerification && !proofVerified && (hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
1858
+ if (deferredLdSignatureError != null) throw deferredLdSignatureError;
1859
+ logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
1860
+ activity: json,
1861
+ recipient,
1862
+ activityId: activity.id?.href,
1863
+ actorId: activity.actorId?.href,
1864
+ keyId: httpSigKey?.id?.href
1865
+ });
1866
+ span.setStatus({
1867
+ code: SpanStatusCode.ERROR,
1868
+ message: `The activity of the portable actor (${activity.actorId?.href}) is not authenticated by a valid Object Integrity Proof.`
1869
+ });
1870
+ return new Response("Activities of portable actors must have valid Object Integrity Proofs.", {
1871
+ status: 401,
1872
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1873
+ });
1874
+ }
1840
1875
  if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
1841
1876
  if (deferredLdSignatureError != null) throw deferredLdSignatureError;
1842
1877
  getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
@@ -1907,6 +1942,19 @@ async function handleInboxInternal(request, parameters, span) {
1907
1942
  tracerProvider,
1908
1943
  idempotencyStrategy: parameters.idempotencyStrategy
1909
1944
  });
1945
+ 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.", {
1946
+ activityId: activity.id.href,
1947
+ inbox: portableInbox.recipient.canonicalInboxId
1948
+ });
1949
+ else try {
1950
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href);
1951
+ } catch (error) {
1952
+ logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
1953
+ activityId: activity.id.href,
1954
+ inbox: portableInbox.recipient.canonicalInboxId,
1955
+ error
1956
+ });
1957
+ }
1910
1958
  if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
1911
1959
  status: 202,
1912
1960
  headers: { "Content-Type": "text/plain; charset=utf-8" }
@@ -1932,6 +1980,12 @@ async function handleInboxInternal(request, parameters, span) {
1932
1980
  headers: { "Content-Type": "text/plain; charset=utf-8" }
1933
1981
  });
1934
1982
  }
1983
+ const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
1984
+ "success",
1985
+ "enqueued",
1986
+ "alreadyProcessed",
1987
+ "unsupportedActivity"
1988
+ ]);
1935
1989
  /**
1936
1990
  * Handles a custom collection request.
1937
1991
  * @template TItem The type of items in the collection.
@@ -2539,6 +2593,260 @@ const MIN_COMPONENTS = [
2539
2593
  "@target-uri",
2540
2594
  "@authority"
2541
2595
  ];
2596
+ /**
2597
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
2598
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
2599
+ * a portable actor, which only the portable actor's signed document can
2600
+ * establish.
2601
+ */
2602
+ function isPortableGatewayKey(key) {
2603
+ return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableUri(key.ownerId);
2604
+ }
2605
+ //#endregion
2606
+ //#region src/federation/portable-inbox.ts
2607
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2608
+ /**
2609
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2610
+ * validates them.
2611
+ * @param options The options.
2612
+ * @returns The resolved options.
2613
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2614
+ * is not positive, if `deadline` is negative or longer
2615
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
2616
+ * if either duration has calendar units, i.e., weeks,
2617
+ * months, or years, whose length depends on the date.
2618
+ */
2619
+ function resolvePortableInboxForwardingOptions(options = {}) {
2620
+ const maxTargets = options.maxTargets ?? 10;
2621
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2622
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
2623
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2624
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
2625
+ const deadlineMs = deadline.total("millisecond");
2626
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2627
+ return {
2628
+ maxTargets,
2629
+ ttl,
2630
+ deadline
2631
+ };
2632
+ }
2633
+ /**
2634
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
2635
+ * of the given actor, which the actor dispatcher returned for the identifier
2636
+ * in the requested inbox path.
2637
+ *
2638
+ * The actor document comes from the application, so it is trusted as is and
2639
+ * its proof is not verified here.
2640
+ * @param actor The actor the actor dispatcher returned.
2641
+ * @param options The requested DID, the canonical form of the requested inbox
2642
+ * ID, and the origin of this server.
2643
+ * @returns The resolution.
2644
+ */
2645
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2646
+ if (actor == null) return {
2647
+ status: "rejected",
2648
+ reason: "notFound"
2649
+ };
2650
+ const id = actor.id;
2651
+ if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
2652
+ status: "rejected",
2653
+ reason: "notPortable"
2654
+ };
2655
+ if (!isSameAuthority(id, authority)) return {
2656
+ status: "rejected",
2657
+ reason: "authorityMismatch"
2658
+ };
2659
+ const inboxId = actor.inboxId;
2660
+ if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
2661
+ status: "rejected",
2662
+ reason: "inboxMismatch"
2663
+ };
2664
+ const gateways = actor.gateways;
2665
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
2666
+ status: "rejected",
2667
+ reason: "notGateway"
2668
+ };
2669
+ return {
2670
+ status: "accepted",
2671
+ recipient: {
2672
+ inboxId,
2673
+ canonicalInboxId,
2674
+ gateways
2675
+ }
2676
+ };
2677
+ }
2678
+ function isSameAuthority(id, authority) {
2679
+ try {
2680
+ return getFe34Origin(id) === authority;
2681
+ } catch (error) {
2682
+ if (error instanceof TypeError) return false;
2683
+ throw error;
2684
+ }
2685
+ }
2686
+ function canonicalize(iri) {
2687
+ try {
2688
+ return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
2689
+ } catch (error) {
2690
+ if (error instanceof TypeError) return null;
2691
+ throw error;
2692
+ }
2693
+ }
2694
+ /**
2695
+ * Forwards an activity received in a portable inbox to the inboxes of the same
2696
+ * portable actor on its other gateways, as FEP-ef61 recommends.
2697
+ *
2698
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2699
+ * each target gateway is claimed with an atomic compare-and-swap before the
2700
+ * activity is handed off, and a claim is never released, even if the hand-off
2701
+ * fails, since a failed hand-off does not prove that nothing was delivered.
2702
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2703
+ * forwarded.
2704
+ *
2705
+ * Forwarded requests are not signed with HTTP Signatures; the receiving
2706
+ * gateways authenticate the activity by its own proof.
2707
+ * @param parameters The parameters.
2708
+ * @returns The target inbox URLs that the activity was handed off to.
2709
+ */
2710
+ async function forwardPortableInboxActivity(parameters) {
2711
+ const logger = getLogger([
2712
+ "fedify",
2713
+ "federation",
2714
+ "inbox"
2715
+ ]);
2716
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2717
+ const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
2718
+ if (kv.cas == null || maxTargets < 1) return [];
2719
+ const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
2720
+ const targets = getForwardingTargets(recipient, excludedOrigins);
2721
+ if (targets.length > maxTargets) {
2722
+ 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.", {
2723
+ inbox: recipient.canonicalInboxId,
2724
+ activityId: activityId.href,
2725
+ max: maxTargets
2726
+ });
2727
+ targets.length = maxTargets;
2728
+ }
2729
+ const claimed = [];
2730
+ for (const { gateway, inbox } of targets) {
2731
+ const key = [
2732
+ ...kvPrefix,
2733
+ recipient.canonicalInboxId,
2734
+ canonicalActivityId,
2735
+ gateway
2736
+ ];
2737
+ if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
2738
+ }
2739
+ if (claimed.length < 1) return [];
2740
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
2741
+ activityId: activityId.href,
2742
+ inbox: recipient.canonicalInboxId,
2743
+ targets: claimed.map((t) => t.href)
2744
+ });
2745
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed);
2746
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
2747
+ return claimed;
2748
+ }
2749
+ function getForwardingTargets(recipient, excludedOrigins) {
2750
+ const logger = getLogger([
2751
+ "fedify",
2752
+ "federation",
2753
+ "inbox"
2754
+ ]);
2755
+ const seen = new Set(excludedOrigins);
2756
+ const targets = [];
2757
+ for (const gateway of recipient.gateways) {
2758
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
2759
+ seen.add(gateway.origin);
2760
+ let inbox;
2761
+ try {
2762
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
2763
+ } catch (error) {
2764
+ if (!(error instanceof TypeError)) throw error;
2765
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
2766
+ gateway: gateway.origin,
2767
+ inbox: recipient.canonicalInboxId
2768
+ });
2769
+ continue;
2770
+ }
2771
+ targets.push({
2772
+ gateway: gateway.origin,
2773
+ inbox
2774
+ });
2775
+ }
2776
+ return targets;
2777
+ }
2778
+ async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
2779
+ const logger = getLogger([
2780
+ "fedify",
2781
+ "federation",
2782
+ "inbox"
2783
+ ]);
2784
+ const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
2785
+ const sends = inboxes.map((inbox) => sendActivity({
2786
+ activity,
2787
+ activityId: activityId.href,
2788
+ activityType,
2789
+ keys: [],
2790
+ inbox,
2791
+ allowPrivateAddress,
2792
+ meterProvider,
2793
+ tracerProvider
2794
+ }).catch((error) => {
2795
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2796
+ activityId: activityId.href,
2797
+ inbox: inbox.href,
2798
+ error
2799
+ });
2800
+ }));
2801
+ let timer;
2802
+ const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
2803
+ timer = setTimeout(() => resolve(true), deadline);
2804
+ })]);
2805
+ clearTimeout(timer);
2806
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
2807
+ activityId: activityId.href,
2808
+ deadline
2809
+ });
2810
+ }
2811
+ async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
2812
+ const logger = getLogger([
2813
+ "fedify",
2814
+ "federation",
2815
+ "inbox"
2816
+ ]);
2817
+ startQueue?.();
2818
+ const started = (/* @__PURE__ */ new Date()).toISOString();
2819
+ const traceContext = {};
2820
+ propagation.inject(context.active(), traceContext);
2821
+ await Promise.all(inboxes.map(async (inbox) => {
2822
+ const message = {
2823
+ type: "outbox",
2824
+ id: crypto.randomUUID(),
2825
+ baseUrl,
2826
+ keys: [],
2827
+ activity,
2828
+ activityId: activityId.href,
2829
+ activityType,
2830
+ inbox: inbox.href,
2831
+ sharedInbox: false,
2832
+ started,
2833
+ attempt: 0,
2834
+ headers: {},
2835
+ traceContext
2836
+ };
2837
+ try {
2838
+ await outboxQueue.enqueue(message);
2839
+ } catch (error) {
2840
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2841
+ activityId: activityId.href,
2842
+ inbox: inbox.href,
2843
+ error
2844
+ });
2845
+ return;
2846
+ }
2847
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
2848
+ }));
2849
+ }
2542
2850
  //#endregion
2543
2851
  //#region src/federation/outgoing-proof.ts
2544
2852
  function isPortableUrl(url) {
@@ -2548,37 +2856,48 @@ function formatKeyIds(candidates) {
2548
2856
  return candidates.map((c) => c.verificationMethod.href).join(", ");
2549
2857
  }
2550
2858
  /**
2859
+ * Rejects an activity of a portable actor whose ID cannot be authenticated by
2860
+ * the actor's DID: FEP-ef61 requires the activity to be a portable object
2861
+ * signed by that DID, so its ID has to be a portable ID with the same DID.
2862
+ * @throws {TypeError} If the activity is performed by a portable actor, but
2863
+ * its ID is not a portable ID of the actor's DID, or its
2864
+ * portable actors do not share a DID.
2865
+ * @internal
2866
+ */
2867
+ function assertPortableActorActivity(activity) {
2868
+ const portableActors = activity.actorIds.filter(isPortableUrl);
2869
+ if (portableActors.length < 1) return;
2870
+ const did = getFe34Origin(portableActors[0]);
2871
+ if (portableActors.some((actorId) => getFe34Origin(actorId) !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
2872
+ if (activity.id == null || !isPortableUrl(activity.id) || getFe34Origin(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
2873
+ }
2874
+ /**
2551
2875
  * Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
2552
2876
  *
2877
+ * Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
2878
+ * never sign proofs. A portable activity, i.e., one with an `ap:` or
2879
+ * `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
2880
+ * whose verification method is a DID URL for its DID, even if it is the only
2881
+ * key; if no key or more than one key qualifies, this function throws.
2882
+ *
2553
2883
  * Outside the map-local compound-proof profile, i.e., when the activity
2554
2884
  * embeds no portable object, every Ed25519 key signs the activity, as it
2555
2885
  * always has. Inside the profile, which accepts exactly one direct proof per
2556
2886
  * map, Fedify creates at most one proof:
2557
2887
  *
2558
2888
  * - An activity that already carries a proof is kept as is.
2559
- * - A single Ed25519 key signs the activity.
2560
- * - With several Ed25519 keys, a portable activity is signed by the one key
2561
- * whose verification method is a DID URL for the activity's DID, the only
2562
- * proof the FEP-ef61 policy can accept. If no key or more than one key
2563
- * qualifies, or the activity is not portable itself, the choice is
2564
- * ambiguous and this function throws.
2889
+ * - A single Ed25519 key signs a non-portable activity.
2890
+ * - With several Ed25519 keys, a non-portable activity is ambiguous, and
2891
+ * this function throws.
2565
2892
  *
2566
- * @throws {TypeError} If a single proof key cannot be chosen.
2893
+ * @throws {TypeError} If a single proof key cannot be chosen, or the activity
2894
+ * is performed by a portable actor but is not a portable
2895
+ * activity of the same DID.
2567
2896
  * @internal
2568
2897
  */
2569
2898
  async function signOutgoingActivity(activity, candidates, options) {
2570
2899
  const { contextLoader, tracerProvider } = options;
2571
- let hasProof = false;
2572
- for await (const _ of activity.getProofs({ contextLoader })) {
2573
- hasProof = true;
2574
- break;
2575
- }
2576
- const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519");
2577
- if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
2578
- activity,
2579
- hasProof,
2580
- proofCreated: false
2581
- };
2900
+ assertPortableActorActivity(activity);
2582
2901
  const sign = async (selected) => {
2583
2902
  for (const { verificationMethod, privateKey } of selected) activity = await signObject(activity, privateKey, verificationMethod, {
2584
2903
  contextLoader,
@@ -2590,6 +2909,18 @@ async function signOutgoingActivity(activity, candidates, options) {
2590
2909
  proofCreated: true
2591
2910
  };
2592
2911
  };
2912
+ let hasProof = false;
2913
+ for await (const _ of activity.getProofs({ contextLoader })) {
2914
+ hasProof = true;
2915
+ break;
2916
+ }
2917
+ const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isCompatibleKeyId(c.verificationMethod));
2918
+ if (!hasProof && activity.id != null && isPortableUrl(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
2919
+ if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
2920
+ activity,
2921
+ hasProof,
2922
+ proofCreated: false
2923
+ };
2593
2924
  if (keys.length < 2 && !hasProof) return await sign(keys);
2594
2925
  if (!containsCompoundPortableObject(await activity.toJsonLd({
2595
2926
  format: "compact",
@@ -2609,15 +2940,26 @@ async function signOutgoingActivity(activity, candidates, options) {
2609
2940
  proofCreated: false
2610
2941
  };
2611
2942
  }
2612
- if (activity.id == null || !isPortableUrl(activity.id)) throw new TypeError(`Cannot choose an Object Integrity Proof key for the activity ${activityId}: it embeds portable objects, so it falls under the map-local compound-proof profile, which accepts exactly one direct proof per map, but ${keys.length} Ed25519 keys were supplied (${formatKeyIds(keys)}). Send it with explicit sender keys that contain exactly one Ed25519 key (RSA keys for HTTP Signatures may stay), or sign it with signObject() before sending it.`);
2613
- const activityDid = getFe34Origin(activity.id);
2614
- const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && haveSameFe34Origin(activity.id, c.verificationMethod));
2615
- if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. The map-local compound-proof profile accepts exactly one direct proof per map, and FEP-ef61 requires that proof's verification method to be a DID URL for the activity's DID. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
2616
- logger.debug("The portable activity {activityId} is signed only by {keyId}, the one Ed25519 key whose verification method matches its DID; the map-local compound-proof profile accepts exactly one direct proof per map.", {
2617
- activityId,
2943
+ throw new TypeError(`Cannot choose an Object Integrity Proof key for the activity ${activityId}: it embeds portable objects, so it falls under the map-local compound-proof profile, which accepts exactly one direct proof per map, but ${keys.length} Ed25519 keys were supplied (${formatKeyIds(keys)}). Send it with explicit sender keys that contain exactly one Ed25519 key (RSA keys for HTTP Signatures may stay), or sign it with signObject() before sending it.`);
2944
+ }
2945
+ /**
2946
+ * Chooses the one Ed25519 key that can sign a portable activity: the key
2947
+ * whose verification method is a DID URL for the activity's DID.
2948
+ * @throws {TypeError} If no key or more than one key qualifies.
2949
+ */
2950
+ function selectPortableActivityKey(activity, activityId, keys) {
2951
+ const activityDid = getFe34Origin(activityId);
2952
+ const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && haveSameFe34Origin(activityId, c.verificationMethod));
2953
+ if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId.href}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. FEP-ef61 requires a portable activity's proof to be made by its DID, and the map-local compound-proof profile accepts exactly one direct proof per map. Gateway keys, whose IDs are compatible identifiers, never sign proofs. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
2954
+ getLogger([
2955
+ "fedify",
2956
+ "federation",
2957
+ "outbox"
2958
+ ]).debug("The portable activity {activityId} is signed only by {keyId}, the one Ed25519 key whose verification method matches its DID.", {
2959
+ activityId: activity.id?.href,
2618
2960
  keyId: eligible[0].verificationMethod.href
2619
2961
  });
2620
- return await sign(eligible);
2962
+ return eligible;
2621
2963
  }
2622
2964
  /**
2623
2965
  * Rejects an outgoing activity whose proofs Fedify's own inbox would reject
@@ -3226,6 +3568,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3226
3568
  kvPrefixes;
3227
3569
  publicKeyTtl;
3228
3570
  httpMessageSignaturesSpecTtl;
3571
+ portableInboxForwarding;
3229
3572
  inboxQueue;
3230
3573
  outboxQueue;
3231
3574
  fanoutQueue;
@@ -3258,6 +3601,7 @@ var FederationImpl = class extends FederationBuilderImpl {
3258
3601
  benchmarkMetricReader;
3259
3602
  benchmarkTriggerOptions;
3260
3603
  #mediaUploaderNoAuthWarned = false;
3604
+ #portableInboxForwardingWarned = false;
3261
3605
  #queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
3262
3606
  #queueDepthGaugeEntries = [];
3263
3607
  #queueDepthGaugeMeterProvider;
@@ -3289,10 +3633,12 @@ var FederationImpl = class extends FederationBuilderImpl {
3289
3633
  acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
3290
3634
  circuitBreaker: ["_fedify", "circuit"],
3291
3635
  taskDeduplication: ["_fedify", "taskDeduplication"],
3636
+ portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
3292
3637
  ...options.kvPrefixes ?? {}
3293
3638
  };
3294
3639
  this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
3295
3640
  this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
3641
+ this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
3296
3642
  if (options.queue == null) {
3297
3643
  this.inboxQueue = void 0;
3298
3644
  this.outboxQueue = void 0;
@@ -3962,8 +4308,8 @@ var FederationImpl = class extends FederationBuilderImpl {
3962
4308
  ]);
3963
4309
  const baseUrl = new URL(message.baseUrl);
3964
4310
  let context = this.#createContext(baseUrl, ctxData);
3965
- if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
3966
- else if (this.sharedInboxKeyDispatcher != null) {
4311
+ if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
4312
+ else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
3967
4313
  const identity = await this.sharedInboxKeyDispatcher(context);
3968
4314
  if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
3969
4315
  }
@@ -4322,9 +4668,10 @@ var FederationImpl = class extends FederationBuilderImpl {
4322
4668
  const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
4323
4669
  const activityId = activity.id.href;
4324
4670
  let rsaKey = null;
4671
+ const ldSignable = !hasPortableActor(activity);
4325
4672
  for (const { keyId, privateKey } of keys) {
4326
4673
  validateCryptoKey(privateKey, "private");
4327
- if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKey = {
4674
+ if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !isCompatibleKeyId(keyId)) rsaKey = {
4328
4675
  keyId,
4329
4676
  privateKey
4330
4677
  };
@@ -4345,7 +4692,8 @@ var FederationImpl = class extends FederationBuilderImpl {
4345
4692
  });
4346
4693
  if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
4347
4694
  assertSupportedCompoundProofShape(jsonLd, activityId);
4348
- if (rsaKey == null) logger.warn("No supported key found to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature. In order to create a Linked Data signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4695
+ if (rsaKey == null && !ldSignable) logger.debug("The activity {activityId} is performed by a portable actor, so it is sent without a Linked Data signature.", { activityId });
4696
+ else if (rsaKey == null) logger.warn("No supported key found to create a Linked Data signature for the activity {activityId}. The activity will be sent without a Linked Data signature. In order to create a Linked Data signature, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
4349
4697
  activityId,
4350
4698
  keys: keys.map((pair) => ({
4351
4699
  keyId: pair.keyId.href,
@@ -4554,6 +4902,15 @@ var FederationImpl = class extends FederationBuilderImpl {
4554
4902
  span,
4555
4903
  metricState
4556
4904
  });
4905
+ if (request.method === "POST") {
4906
+ const portable = parsePortableGatewayRequest(url);
4907
+ if (portable != null) return await this.#fetchPortableInbox(request, portable, {
4908
+ onNotFound,
4909
+ contextData,
4910
+ span,
4911
+ metricState
4912
+ });
4913
+ }
4557
4914
  const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
4558
4915
  if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
4559
4916
  onNotFound,
@@ -4705,35 +5062,17 @@ var FederationImpl = class extends FederationBuilderImpl {
4705
5062
  onNotFound
4706
5063
  });
4707
5064
  context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
4708
- case "sharedInbox": {
5065
+ case "sharedInbox":
4709
5066
  if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
4710
5067
  const identity = await this.sharedInboxKeyDispatcher(context);
4711
5068
  if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
4712
5069
  }
4713
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
4714
- const inboxContextFactory = context.toInboxContext.bind(context);
4715
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
4716
- return await handleInbox(request, {
5070
+ return await this.#handleInbox(request, {
4717
5071
  recipient: route.values.identifier ?? null,
4718
5072
  context,
4719
- inboxContextFactory,
4720
- kv: this.kv,
4721
- kvPrefixes: this.kvPrefixes,
4722
- publicKeyTtl: this.publicKeyTtl,
4723
- queue: this.inboxQueue,
4724
- actorDispatcher: this.actorCallbacks?.dispatcher,
4725
- inboxListeners: this.inboxListeners,
4726
- inboxErrorHandler: this.inboxErrorHandler,
4727
- unverifiedActivityHandler: this.unverifiedActivityHandler,
4728
- onNotFound,
4729
- signatureTimeWindow: this.signatureTimeWindow,
4730
- skipSignatureVerification: this.skipSignatureVerification,
4731
- inboxChallengePolicy: this.inboxChallengePolicy,
4732
- meterProvider: this.meterProvider,
4733
- tracerProvider: this.tracerProvider,
4734
- idempotencyStrategy: this.idempotencyStrategy
5073
+ contextData,
5074
+ onNotFound
4735
5075
  });
4736
- }
4737
5076
  case "following": return await handleCollection(request, {
4738
5077
  name: "following",
4739
5078
  identifier: route.values.identifier,
@@ -4886,6 +5225,113 @@ var FederationImpl = class extends FederationBuilderImpl {
4886
5225
  onNotFound
4887
5226
  });
4888
5227
  }
5228
+ async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
5229
+ if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
5230
+ const inboxContextFactory = context.toInboxContext.bind(context);
5231
+ inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
5232
+ return await handleInbox(request, {
5233
+ recipient,
5234
+ context,
5235
+ inboxContextFactory,
5236
+ kv: this.kv,
5237
+ kvPrefixes: this.kvPrefixes,
5238
+ publicKeyTtl: this.publicKeyTtl,
5239
+ queue: this.inboxQueue,
5240
+ actorDispatcher: this.actorCallbacks?.dispatcher,
5241
+ inboxListeners: this.inboxListeners,
5242
+ inboxErrorHandler: this.inboxErrorHandler,
5243
+ unverifiedActivityHandler: this.unverifiedActivityHandler,
5244
+ onNotFound,
5245
+ signatureTimeWindow: this.signatureTimeWindow,
5246
+ skipSignatureVerification: this.skipSignatureVerification,
5247
+ inboxChallengePolicy: this.inboxChallengePolicy,
5248
+ meterProvider: this.meterProvider,
5249
+ tracerProvider: this.tracerProvider,
5250
+ idempotencyStrategy: this.idempotencyStrategy,
5251
+ portableInbox
5252
+ });
5253
+ }
5254
+ async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
5255
+ const logger = getLogger([
5256
+ "fedify",
5257
+ "federation",
5258
+ "inbox"
5259
+ ]);
5260
+ if (portable.type === "malformed") {
5261
+ metricState.endpoint = "not_found";
5262
+ logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
5263
+ url: request.url,
5264
+ error: portable.error
5265
+ });
5266
+ return new Response("Malformed portable inbox ID.", {
5267
+ status: 400,
5268
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
5269
+ });
5270
+ }
5271
+ const route = this.router.route(portable.path);
5272
+ if (route == null || route.name !== "inbox") {
5273
+ metricState.endpoint = "not_found";
5274
+ return await onNotFound(request);
5275
+ }
5276
+ metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
5277
+ metricState.endpoint = "inbox";
5278
+ span.updateName(`${request.method} ${metricState.routeTemplate}`);
5279
+ const identifier = route.values.identifier;
5280
+ let context = this.#createContext(request, contextData);
5281
+ const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
5282
+ const localOrigin = context.canonicalOrigin;
5283
+ const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
5284
+ authority: portable.portableRequest.authority,
5285
+ canonicalInboxId: portable.canonicalId,
5286
+ localOrigin
5287
+ });
5288
+ if (resolution.status === "rejected") {
5289
+ logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
5290
+ inbox: portable.canonicalId,
5291
+ identifier,
5292
+ reason: resolution.reason
5293
+ });
5294
+ return await onNotFound(request);
5295
+ }
5296
+ const { recipient } = resolution;
5297
+ if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
5298
+ const excludedOrigins = [localOrigin, new URL(request.url).origin];
5299
+ return await this.#handleInbox(request, {
5300
+ recipient: identifier,
5301
+ context,
5302
+ contextData,
5303
+ onNotFound,
5304
+ portableInbox: {
5305
+ recipient,
5306
+ forward: async (activity, activityId, activityType) => {
5307
+ if (this.portableInboxForwarding.maxTargets < 1) return;
5308
+ if (this.kv.cas == null) {
5309
+ if (!this.#portableInboxForwardingWarned) {
5310
+ this.#portableInboxForwardingWarned = true;
5311
+ 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.");
5312
+ }
5313
+ return;
5314
+ }
5315
+ await forwardPortableInboxActivity({
5316
+ recipient,
5317
+ activity,
5318
+ activityId,
5319
+ activityType,
5320
+ excludedOrigins,
5321
+ baseUrl: context.origin,
5322
+ kv: this.kv,
5323
+ kvPrefix: this.kvPrefixes.portableInboxForwarding,
5324
+ outboxQueue: this.outboxQueue,
5325
+ startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
5326
+ allowPrivateAddress: this.allowPrivateAddress,
5327
+ options: this.portableInboxForwarding,
5328
+ meterProvider: this.meterProvider,
5329
+ tracerProvider: this.tracerProvider
5330
+ });
5331
+ }
5332
+ }
5333
+ });
5334
+ }
4889
5335
  async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
4890
5336
  metricState.endpoint = "hashlink_media";
4891
5337
  metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
@@ -5116,6 +5562,12 @@ var ContextImpl = class ContextImpl {
5116
5562
  if (path == null) throw new RouterError("No inbox path registered.");
5117
5563
  return new URL(path, this.canonicalOrigin);
5118
5564
  }
5565
+ getPortableInboxUri(identifier, authority) {
5566
+ const path = this.federation.router.build("inbox", { identifier });
5567
+ if (path == null) throw new RouterError("No inbox path registered.");
5568
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5569
+ return buildPortableUri(authority, path);
5570
+ }
5119
5571
  getFollowingUri(identifier) {
5120
5572
  const path = this.federation.router.build("following", { identifier });
5121
5573
  if (path == null) throw new RouterError("No following collection path registered.");
@@ -5221,14 +5673,15 @@ var ContextImpl = class ContextImpl {
5221
5673
  getActorKeyPairsIdentifier: identifier,
5222
5674
  actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
5223
5675
  });
5676
+ const portableActor = await this.getPortableActorKeyOwner(identifier);
5224
5677
  let keyPairs;
5225
5678
  try {
5226
- keyPairs = await this.getKeyPairsFromIdentifier(identifier);
5679
+ keyPairs = await this.getKeyPairsFromIdentifier(identifier, portableActor);
5227
5680
  } catch (_) {
5228
5681
  logger.warn("No actor key pairs dispatcher registered.");
5229
5682
  return [];
5230
5683
  }
5231
- const owner = this.getActorUri(identifier);
5684
+ const owner = portableActor?.id ?? this.getActorUri(identifier);
5232
5685
  const result = [];
5233
5686
  let i = 1;
5234
5687
  for (const keyPair of keyPairs) {
@@ -5240,7 +5693,7 @@ var ContextImpl = class ContextImpl {
5240
5693
  publicKey: keyPair.publicKey
5241
5694
  }),
5242
5695
  multikey: new Multikey({
5243
- id: new URL(`#multikey-${i}`, owner),
5696
+ id: portableActor == null ? new URL(`#multikey-${i}`, owner) : keyPair.keyId,
5244
5697
  controller: owner,
5245
5698
  publicKey: keyPair.publicKey
5246
5699
  })
@@ -5250,7 +5703,26 @@ var ContextImpl = class ContextImpl {
5250
5703
  }
5251
5704
  return result;
5252
5705
  }
5253
- async getKeyPairsFromIdentifier(identifier) {
5706
+ /**
5707
+ * Resolves the portable actor that the actor with the given identifier is,
5708
+ * through the portable actor ID mapper, along with the base of its gateway
5709
+ * key IDs on this server.
5710
+ * @param identifier The actor's identifier.
5711
+ * @returns The portable actor's ID and key ID base, or `null` if the actor
5712
+ * is not portable.
5713
+ * @throws {TypeError} If the mapper returns an invalid portable actor ID.
5714
+ */
5715
+ async getPortableActorKeyOwner(identifier) {
5716
+ const mapper = this.federation.actorCallbacks?.portableActorIdMapper;
5717
+ if (mapper == null) return null;
5718
+ const id = await mapper(this, identifier);
5719
+ if (id == null) return null;
5720
+ return {
5721
+ id,
5722
+ keyBase: getGatewayKeyBase(id, this.canonicalOrigin)
5723
+ };
5724
+ }
5725
+ async getKeyPairsFromIdentifier(identifier, portableActor) {
5254
5726
  const logger = getLogger([
5255
5727
  "fedify",
5256
5728
  "federation",
@@ -5267,6 +5739,8 @@ var ContextImpl = class ContextImpl {
5267
5739
  }
5268
5740
  throw error;
5269
5741
  }
5742
+ if (portableActor === void 0) portableActor = await this.getPortableActorKeyOwner(identifier);
5743
+ const keyBase = portableActor?.keyBase ?? actorUri;
5270
5744
  const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
5271
5745
  ...this,
5272
5746
  invokedFromActorKeyPairsDispatcher: { identifier }
@@ -5277,7 +5751,7 @@ var ContextImpl = class ContextImpl {
5277
5751
  for (const keyPair of keyPairs) {
5278
5752
  result.push({
5279
5753
  ...keyPair,
5280
- keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, actorUri)
5754
+ keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, keyBase)
5281
5755
  });
5282
5756
  i++;
5283
5757
  }
@@ -5696,6 +6170,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
5696
6170
  getPortableObjectUri(cls, values, authority) {
5697
6171
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
5698
6172
  }
6173
+ getPortableInboxUri(identifier, authority) {
6174
+ return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
6175
+ }
5699
6176
  async getActor(identifier, options) {
5700
6177
  if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
5701
6178
  if (this.#invokedFromActorDispatcher != null) getLogger([
@@ -6086,4 +6563,4 @@ function getRequestId(request) {
6086
6563
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
6087
6564
  }
6088
6565
  //#endregion
6089
- 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 };
6566
+ 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 };