@fedify/fedify 2.4.0-dev.2093 → 2.4.0-dev.2103

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 (84) hide show
  1. package/dist/{builder-Ld4Y9ymR.mjs → builder-CHht75pX.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-CTx_lewQ.mjs → compound-proof-DKKH_Nr1.mjs} +2 -2
  6. package/dist/{context-BSywYspi.d.ts → context-CfJE_oQu.d.ts} +78 -13
  7. package/dist/{context-DlZhxdFT.mjs → context-CoNRxGYp.mjs} +2 -1
  8. package/dist/{context-CZjZYXFC.d.cts → context-mDTxTw-S.d.cts} +78 -13
  9. package/dist/{deno-8t8k_6Yv.mjs → deno-CkjiYK_Z.mjs} +1 -1
  10. package/dist/{docloader-DJKcsAiF.mjs → docloader-C1hAJnWc.mjs} +2 -2
  11. package/dist/federation/builder.test.mjs +1 -1
  12. package/dist/federation/compound-fanout.test.mjs +3 -3
  13. package/dist/federation/handler-compound-proof.test.mjs +4 -4
  14. package/dist/federation/handler.test.mjs +6 -6
  15. package/dist/federation/hashlink-media.test.mjs +2 -2
  16. package/dist/federation/idempotency.test.mjs +2 -2
  17. package/dist/federation/metrics.test.mjs +1 -1
  18. package/dist/federation/middleware.test.mjs +8 -8
  19. package/dist/federation/mod.cjs +1 -1
  20. package/dist/federation/mod.d.cts +2 -2
  21. package/dist/federation/mod.d.ts +2 -2
  22. package/dist/federation/mod.js +1 -1
  23. package/dist/federation/outgoing-proof.test.mjs +5 -5
  24. package/dist/federation/portable-actor.test.d.mts +2 -0
  25. package/dist/federation/portable-actor.test.mjs +446 -0
  26. package/dist/federation/portable-gateway-keys.test.mjs +4 -4
  27. package/dist/federation/portable-inbox.test.mjs +396 -10
  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-aSHO04ob.cjs → federation-C1dL6SiT.cjs} +1267 -1086
  35. package/dist/{http-B2ftY0Ha.mjs → http-BU-8fUrz.mjs} +82 -7
  36. package/dist/{http-DSaSAN7X.cjs → http-Cala2Grs.cjs} +89 -2
  37. package/dist/{http-DyT7DP1Y.js → http-tTlTeFa0.js} +78 -3
  38. package/dist/{key-Dwm4VswT.mjs → key-I8gLBkcf.mjs} +2 -2
  39. package/dist/{kv-cache-BSJAlP4C.cjs → kv-cache-Bl5EBFrU.cjs} +1 -1
  40. package/dist/{kv-cache-DmroAz3M.js → kv-cache-CxQu_cd4.js} +1 -1
  41. package/dist/{kv-cache-D0yYukqk.mjs → kv-cache-DJFoGRWQ.mjs} +1 -1
  42. package/dist/{ld-BYZOoDzZ.mjs → ld-wiJbqJHa.mjs} +3 -3
  43. package/dist/{metrics-B7YwKTFr.mjs → metrics-BCAa5-kE.mjs} +1 -1
  44. package/dist/{middleware-BNeqKSsL.mjs → middleware-6p2dYEVA.mjs} +1 -1
  45. package/dist/{middleware-Blq0882d.js → middleware-B6IVTVL0.js} +1362 -1181
  46. package/dist/{middleware-E_0IH72m.mjs → middleware-FnITXhtz.mjs} +549 -368
  47. package/dist/{mod-JzkaEXsi.d.cts → mod-B3OzOobN.d.cts} +1 -1
  48. package/dist/{mod-B2h2KY0q.d.ts → mod-BYvPz_T0.d.ts} +1 -1
  49. package/dist/mod.cjs +5 -5
  50. package/dist/mod.d.cts +2 -2
  51. package/dist/mod.d.ts +2 -2
  52. package/dist/mod.js +5 -5
  53. package/dist/nodeinfo/handler.test.mjs +2 -2
  54. package/dist/{owner-Cgx2Dc7I.mjs → owner-BJp47LxN.mjs} +3 -3
  55. package/dist/{owner-szIZl4qI.cjs → owner-DCt8W_Cv.cjs} +1 -1
  56. package/dist/{owner-DAGSC5gX.js → owner-j5yrbXS1.js} +1 -1
  57. package/dist/{portable-key-Cn1WJSN1.mjs → portable-key-B5oKE_wZ.mjs} +1 -1
  58. package/dist/{proof-DVXC6nYz.mjs → proof-BE4T5zF6.mjs} +4 -4
  59. package/dist/{send-D-hOvYKy.mjs → send-BFITuKHw.mjs} +3 -3
  60. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  61. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  62. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  63. package/dist/sig/compound-proof.test.mjs +2 -2
  64. package/dist/sig/http.test.mjs +72 -2
  65. package/dist/sig/key.test.mjs +1 -1
  66. package/dist/sig/ld.test.mjs +2 -2
  67. package/dist/sig/mod.cjs +3 -3
  68. package/dist/sig/mod.js +3 -3
  69. package/dist/sig/owner.test.mjs +1 -1
  70. package/dist/sig/portable-collection.test.mjs +2 -2
  71. package/dist/sig/portable-key.test.mjs +3 -3
  72. package/dist/sig/proof.test.mjs +3 -3
  73. package/dist/{sig-B5Ell7pO.js → sig-CJHdGk9w.js} +2 -2
  74. package/dist/{sig-CXOKN4Br.cjs → sig-CRTRoouH.cjs} +2 -2
  75. package/dist/{temporal-CqpKGEPk.mjs → temporal-CyDDbE4i.mjs} +1 -1
  76. package/dist/testing/kv.test.mjs +1 -1
  77. package/dist/testing/mod.d.mts +78 -13
  78. package/dist/testing/mod.mjs +2 -2
  79. package/dist/{testing-CO_cEvQc.mjs → testing-BWq1dCzx.mjs} +1 -1
  80. package/dist/utils/docloader.test.mjs +2 -2
  81. package/dist/utils/kv-cache.test.mjs +1 -1
  82. package/dist/utils/mod.cjs +1 -1
  83. package/dist/utils/mod.js +1 -1
  84. package/package.json +7 -7
@@ -1,28 +1,28 @@
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-8t8k_6Yv.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-B7YwKTFr.mjs";
4
+ import { n as version, t as name } from "./deno-CkjiYK_Z.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-BCAa5-kE.mjs";
6
6
  import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
7
7
  import { a as isCompatibleKeyId, c as isPortableUri, i as hasPortableActor, n as getGatewayKeyBase, r as getPortableDid, s as isPortableId, t as getCanonicalPortableId } from "./portable-key-id-Bva5iT3W.mjs";
8
- import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-Dwm4VswT.mjs";
8
+ import { o as importJwk, s as validateCryptoKey, t as exportJwk } from "./key-I8gLBkcf.mjs";
9
9
  import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
10
- 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-BYZOoDzZ.mjs";
11
- import { i as signObject, l as verifyPortableObjectProofWithRoot, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, t as InvalidPortableObjectIdError } from "./proof-DVXC6nYz.mjs";
12
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-B2ftY0Ha.mjs";
13
- import { t as getAuthenticatedDocumentLoader } from "./docloader-DJKcsAiF.mjs";
14
- import { n as kvCache } from "./kv-cache-D0yYukqk.mjs";
10
+ 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-wiJbqJHa.mjs";
11
+ import { i as signObject, l as verifyPortableObjectProofWithRoot, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, t as InvalidPortableObjectIdError } from "./proof-BE4T5zF6.mjs";
12
+ import { a as listRequestSignatures, c as selectRequestSignature, d as verifyRequest, f as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-BU-8fUrz.mjs";
13
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-C1hAJnWc.mjs";
14
+ import { n as kvCache } from "./kv-cache-DJFoGRWQ.mjs";
15
15
  import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DpnpgjyX.mjs";
16
16
  import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
17
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Cgx2Dc7I.mjs";
18
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-D-hOvYKy.mjs";
19
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Ld4Y9ymR.mjs";
17
+ import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-BJp47LxN.mjs";
18
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-BFITuKHw.mjs";
19
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-CHht75pX.mjs";
20
20
  import { t as CircuitBreaker } from "./circuit-breaker-C5NqoLBJ.mjs";
21
21
  import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
22
- import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-CTx_lewQ.mjs";
22
+ import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-DKKH_Nr1.mjs";
23
23
  import { t as KvKeyCache } from "./keycache-BUDHB5rY.mjs";
24
24
  import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
25
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CqpKGEPk.mjs";
25
+ import { t as hasMalformedKnownTemporalLiteral } from "./temporal-CyDDbE4i.mjs";
26
26
  import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
27
27
  import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
28
28
  import { getLogger, withContext } from "@logtape/logtape";
@@ -522,6 +522,8 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
522
522
  const PORTABLE_OBJECT_CONTENT_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
523
523
  const GATEWAY_OBJECT_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
524
524
  const BARE_DID_PATTERN = /^did:[a-z0-9]+:[^/?#]+$/i;
525
+ const DID_KEY_PREFIX = "did:key:";
526
+ const BASE58BTC_MULTIBASE_PATTERN = /^z[1-9A-HJ-NP-Za-km-z]+$/;
525
527
  const GATEWAY_PATH_PREFIX = "/.well-known/apgateway/";
526
528
  const HASHLINK_SCHEME_PATTERN = /^hl(?::|%3A)/i;
527
529
  /**
@@ -557,6 +559,15 @@ function parsePortableGatewayRequest(url) {
557
559
  throw error;
558
560
  }
559
561
  const authority = getFe34Origin(id);
562
+ try {
563
+ assertBase58BtcDidKey(authority);
564
+ } catch (error) {
565
+ if (error instanceof TypeError) return {
566
+ type: "malformed",
567
+ error
568
+ };
569
+ throw error;
570
+ }
560
571
  const href = id.href;
561
572
  return {
562
573
  type: "object",
@@ -575,12 +586,26 @@ function parsePortableGatewayRequest(url) {
575
586
  * @param authority The bare DID, e.g., `did:key:z6Mk...`.
576
587
  * @param path The object path, e.g., `/notes/123`.
577
588
  * @returns The portable ID.
578
- * @throws {TypeError} If the authority is not a bare DID.
589
+ * @throws {TypeError} If the authority is not a bare DID, or it is
590
+ * a `did:key` DID that is not encoded in base58-btc.
579
591
  */
580
592
  function buildPortableUri(authority, path) {
581
593
  if (typeof authority !== "string" || !BARE_DID_PATTERN.test(authority)) throw new TypeError("The authority of a portable ID must be a DID without a path, query, or fragment.");
582
- getFe34Origin(authority);
583
- return parseIri(`ap+ef61://${authority}${path}`);
594
+ const did = getFe34Origin(authority);
595
+ assertBase58BtcDidKey(did);
596
+ return parseIri(`ap+ef61://${did}${path}`);
597
+ }
598
+ /**
599
+ * Checks that a normalized DID, if it is a `did:key` DID, is encoded in
600
+ * base58-btc, as FEP-ef61 requires so that the same key does not yield two
601
+ * portable IDs. This only checks the encoding, not whether the DID is
602
+ * a valid key.
603
+ * @param did The DID normalized by `getFe34Origin()`.
604
+ * @throws {TypeError} If the DID is a `did:key` DID that is not encoded in
605
+ * base58-btc.
606
+ */
607
+ function assertBase58BtcDidKey(did) {
608
+ if (did.startsWith(DID_KEY_PREFIX) && !BASE58BTC_MULTIBASE_PATTERN.test(did.slice(8))) throw new TypeError("The did:key authority of a portable ID must be encoded in base58-btc, i.e., start with z.");
584
609
  }
585
610
  /**
586
611
  * Recognizes an FEP-ef61 gateway request for a resource addressed by
@@ -632,6 +657,415 @@ function parseHashlinkGatewayRequest(url) {
632
657
  };
633
658
  }
634
659
  //#endregion
660
+ //#region src/federation/portable-inbox.ts
661
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
662
+ /**
663
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
664
+ * validates them.
665
+ * @param options The options.
666
+ * @returns The resolved options.
667
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
668
+ * is not positive, if `deadline` is negative or longer
669
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
670
+ * if either duration has calendar units, i.e., weeks,
671
+ * months, or years, whose length depends on the date.
672
+ */
673
+ function resolvePortableInboxForwardingOptions(options = {}) {
674
+ const maxTargets = options.maxTargets ?? 10;
675
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
676
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
677
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
678
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
679
+ const deadlineMs = deadline.total("millisecond");
680
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
681
+ return {
682
+ maxTargets,
683
+ ttl,
684
+ deadline
685
+ };
686
+ }
687
+ /**
688
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
689
+ * of the given actor, which the actor dispatcher returned for the identifier
690
+ * in the requested inbox path.
691
+ *
692
+ * The actor document comes from the application, so it is trusted as is and
693
+ * its proof is not verified here.
694
+ * @param actor The actor the actor dispatcher returned.
695
+ * @param options The requested DID, the canonical form of the requested inbox
696
+ * ID, and the origin of this server.
697
+ * @returns The resolution.
698
+ */
699
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
700
+ if (actor == null) return {
701
+ status: "rejected",
702
+ reason: "notFound"
703
+ };
704
+ const id = actor.id;
705
+ const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
706
+ if (id == null || did == null) return {
707
+ status: "rejected",
708
+ reason: "notPortable"
709
+ };
710
+ if (did !== authority) return {
711
+ status: "rejected",
712
+ reason: "authorityMismatch"
713
+ };
714
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
715
+ if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
716
+ status: "rejected",
717
+ reason: "inboxMismatch"
718
+ };
719
+ const gateways = actor.gateways;
720
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
721
+ status: "rejected",
722
+ reason: "notGateway"
723
+ };
724
+ return {
725
+ status: "accepted",
726
+ recipient: {
727
+ actorId: id,
728
+ inboxId,
729
+ canonicalInboxId,
730
+ gateways
731
+ }
732
+ };
733
+ }
734
+ /**
735
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
736
+ * identifier, keeping its query.
737
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
738
+ */
739
+ function toPortableUri(id) {
740
+ if (isPortableUri(id)) return id;
741
+ try {
742
+ return fromCompatibleEf61Id(id);
743
+ } catch (error) {
744
+ if (error instanceof TypeError) return null;
745
+ throw error;
746
+ }
747
+ }
748
+ const RFC9421_DELIVERY_COMPONENTS = [
749
+ "@method",
750
+ "@target-uri",
751
+ "content-digest"
752
+ ];
753
+ const DRAFT_DELIVERY_COMPONENTS = [
754
+ "(request-target)",
755
+ "host",
756
+ "digest"
757
+ ];
758
+ /**
759
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
760
+ * body of a request, which both Fedify and Mitra sign when forwarding.
761
+ * @param signature The signature.
762
+ * @returns `true` if the signature covers them.
763
+ */
764
+ function coversDelivery(signature) {
765
+ return (signature.label == null ? DRAFT_DELIVERY_COMPONENTS : RFC9421_DELIVERY_COMPONENTS).every((name) => signature.components.some((c) => c.value === name && Object.keys(c.params).length < 1));
766
+ }
767
+ /**
768
+ * Forwards an activity received in a portable inbox to the inboxes of the same
769
+ * portable actor on its other gateways, as FEP-ef61 recommends.
770
+ *
771
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
772
+ * each target gateway is claimed with an atomic compare-and-swap before the
773
+ * activity is handed off, and a claim is never released, even if the hand-off
774
+ * fails, since a failed hand-off does not prove that nothing was delivered.
775
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
776
+ * forwarded.
777
+ *
778
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
779
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
780
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
781
+ * otherwise. Either way, the receiving gateways authenticate the activity by
782
+ * its own proof.
783
+ *
784
+ * If the delivery is signed with a gateway key of the recipient by one of its
785
+ * other gateways, that gateway has the activity already, so it is claimed
786
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
787
+ * @param parameters The parameters.
788
+ * @returns The target inbox URLs that the activity was handed off to.
789
+ */
790
+ async function forwardPortableInboxActivity(parameters) {
791
+ const logger = getLogger([
792
+ "fedify",
793
+ "federation",
794
+ "inbox"
795
+ ]);
796
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
797
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
798
+ const { maxTargets, ttl } = options;
799
+ if (kv.cas == null || maxTargets < 1) return [];
800
+ const deadline = Date.now() + options.deadline.total("millisecond");
801
+ const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
802
+ const claimKey = (gateway) => [
803
+ ...kvPrefix,
804
+ recipient.canonicalInboxId,
805
+ canonicalActivityId,
806
+ gateway
807
+ ];
808
+ let targets = getForwardingTargets(recipient, excludedOrigins);
809
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
810
+ if (sender != null) {
811
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
812
+ activityId: activityId.href,
813
+ inbox: recipient.canonicalInboxId,
814
+ gateway: sender
815
+ });
816
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
817
+ targets = targets.filter((t) => t.gateway !== sender);
818
+ }
819
+ if (targets.length > maxTargets) {
820
+ 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.", {
821
+ inbox: recipient.canonicalInboxId,
822
+ activityId: activityId.href,
823
+ max: maxTargets
824
+ });
825
+ targets.length = maxTargets;
826
+ }
827
+ const claimed = [];
828
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
829
+ if (claimed.length < 1) return [];
830
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
831
+ activityId: activityId.href,
832
+ inbox: recipient.canonicalInboxId,
833
+ targets: claimed.map((t) => t.href)
834
+ });
835
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
836
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
837
+ return claimed;
838
+ }
839
+ /**
840
+ * Identifies the gateway of the recipient that forwarded a delivery to its
841
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
842
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
843
+ * an actor. Since the activity is authenticated by its own proof, the
844
+ * signature only tells which gateway to skip, and a delivery whose signature
845
+ * is invalid or cannot be verified is still forwarded to every gateway.
846
+ *
847
+ * Verifying the signature fetches the recipient's actor document from the
848
+ * sending gateway, as gateway keys are not cached, so only the first signature
849
+ * that may save a request is verified: one that covers the delivery and names
850
+ * a gateway key of the recipient on one of the forwarding targets that the
851
+ * activity has not been forwarded to yet.
852
+ * @param parameters The forwarding parameters.
853
+ * @param targets The origins of the gateways to forward to.
854
+ * @param claimKey Gets the key of the claim for a gateway.
855
+ * @param deadline The time by which identification has to finish, in
856
+ * milliseconds since the epoch; afterwards, the sending
857
+ * gateway is left unidentified.
858
+ * @returns The origin of the sending gateway, or `null` if it is not
859
+ * identified.
860
+ */
861
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
862
+ if (signatures == null || signatures.length < 1) return null;
863
+ const logger = getLogger([
864
+ "fedify",
865
+ "federation",
866
+ "inbox"
867
+ ]);
868
+ const actorId = getCanonicalPortableId(recipient.actorId);
869
+ if (actorId == null) return null;
870
+ const isGatewayKeyOfRecipient = (keyId) => {
871
+ if (!isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
872
+ const base = new URL(keyId.href);
873
+ base.hash = "";
874
+ return getCanonicalPortableId(base) === actorId;
875
+ };
876
+ let signature;
877
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
878
+ signature = candidate;
879
+ break;
880
+ }
881
+ if (signature == null) return null;
882
+ const { keyId } = signature;
883
+ const logProperties = {
884
+ activityId: activityId.href,
885
+ inbox: recipient.canonicalInboxId,
886
+ keyId: keyId.href
887
+ };
888
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
889
+ if (key == null) logger.debug("The HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} is not verified with the key {keyId}; forwarding it without identifying the sending gateway.", logProperties);
890
+ return key;
891
+ }, (error) => {
892
+ logger.debug("Failed to verify the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} with the key {keyId}; forwarding it without identifying the sending gateway:\n{error}", {
893
+ ...logProperties,
894
+ error
895
+ });
896
+ return null;
897
+ });
898
+ let timer;
899
+ const key = await Promise.race([verification, new Promise((resolve) => {
900
+ timer = setTimeout(() => {
901
+ logger.debug("Verifying the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} did not finish in time; forwarding it without identifying the sending gateway.", logProperties);
902
+ resolve(null);
903
+ }, Math.max(0, deadline - Date.now()));
904
+ })]);
905
+ clearTimeout(timer);
906
+ if (key == null) return null;
907
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !isPortableId(key.ownerId) || getCanonicalPortableId(key.ownerId) !== actorId) {
908
+ logger.debug("The delivery of activity {activityId} to the portable inbox {inbox} is not signed with a gateway key of the recipient; forwarding it without identifying the sending gateway.", logProperties);
909
+ return null;
910
+ }
911
+ return key.id.origin;
912
+ }
913
+ function getForwardingTargets(recipient, excludedOrigins) {
914
+ const logger = getLogger([
915
+ "fedify",
916
+ "federation",
917
+ "inbox"
918
+ ]);
919
+ const seen = new Set(excludedOrigins);
920
+ const targets = [];
921
+ for (const gateway of recipient.gateways) {
922
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
923
+ seen.add(gateway.origin);
924
+ let inbox;
925
+ try {
926
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
927
+ } catch (error) {
928
+ if (!(error instanceof TypeError)) throw error;
929
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
930
+ gateway: gateway.origin,
931
+ inbox: recipient.canonicalInboxId
932
+ });
933
+ continue;
934
+ }
935
+ targets.push({
936
+ gateway: gateway.origin,
937
+ inbox
938
+ });
939
+ }
940
+ return targets;
941
+ }
942
+ /**
943
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
944
+ * key, as {@link sendActivity} signs requests only with such a key. Since
945
+ * the targets have already been claimed, the activity is forwarded unsigned
946
+ * rather than not at all if the key cannot be resolved or used.
947
+ */
948
+ async function resolveForwardingKey({ getKeys, activityId }) {
949
+ if (getKeys == null) return null;
950
+ const logger = getLogger([
951
+ "fedify",
952
+ "federation",
953
+ "inbox"
954
+ ]);
955
+ let key;
956
+ try {
957
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
958
+ } catch (error) {
959
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
960
+ activityId: activityId.href,
961
+ error
962
+ });
963
+ return null;
964
+ }
965
+ if (key == null) return null;
966
+ try {
967
+ validateCryptoKey(key.privateKey, "private");
968
+ } catch (error) {
969
+ if (!(error instanceof TypeError)) throw error;
970
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
971
+ keyId: key.keyId.href,
972
+ activityId: activityId.href,
973
+ error
974
+ });
975
+ return null;
976
+ }
977
+ return key;
978
+ }
979
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
980
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
981
+ const logger = getLogger([
982
+ "fedify",
983
+ "federation",
984
+ "inbox"
985
+ ]);
986
+ const deadline = Math.max(0, deadlineAt - Date.now());
987
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
988
+ activity,
989
+ activityId: activityId.href,
990
+ activityType,
991
+ keys: key == null ? [] : [key],
992
+ inbox,
993
+ allowPrivateAddress,
994
+ specDeterminer,
995
+ meterProvider,
996
+ tracerProvider
997
+ }).catch((error) => {
998
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
999
+ activityId: activityId.href,
1000
+ inbox: inbox.href,
1001
+ error
1002
+ });
1003
+ }))));
1004
+ let timer;
1005
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
1006
+ timer = setTimeout(() => resolve(true), deadline);
1007
+ })]);
1008
+ clearTimeout(timer);
1009
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
1010
+ activityId: activityId.href,
1011
+ deadline
1012
+ });
1013
+ }
1014
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
1015
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
1016
+ const logger = getLogger([
1017
+ "fedify",
1018
+ "federation",
1019
+ "inbox"
1020
+ ]);
1021
+ const key = await resolveForwardingKey(parameters);
1022
+ const keys = [];
1023
+ if (key != null) try {
1024
+ keys.push({
1025
+ keyId: key.keyId.href,
1026
+ privateKey: await exportJwk(key.privateKey)
1027
+ });
1028
+ } catch (error) {
1029
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
1030
+ keyId: key.keyId.href,
1031
+ activityId: activityId.href,
1032
+ error
1033
+ });
1034
+ }
1035
+ startQueue?.();
1036
+ const started = (/* @__PURE__ */ new Date()).toISOString();
1037
+ const traceContext = {};
1038
+ propagation.inject(context.active(), traceContext);
1039
+ await Promise.all(inboxes.map(async (inbox) => {
1040
+ const message = {
1041
+ type: "outbox",
1042
+ id: crypto.randomUUID(),
1043
+ baseUrl,
1044
+ keys,
1045
+ activity,
1046
+ activityId: activityId.href,
1047
+ activityType,
1048
+ inbox: inbox.href,
1049
+ sharedInbox: false,
1050
+ started,
1051
+ attempt: 0,
1052
+ headers: {},
1053
+ traceContext
1054
+ };
1055
+ try {
1056
+ await outboxQueue.enqueue(message);
1057
+ } catch (error) {
1058
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
1059
+ activityId: activityId.href,
1060
+ inbox: inbox.href,
1061
+ error
1062
+ });
1063
+ return;
1064
+ }
1065
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
1066
+ }));
1067
+ }
1068
+ //#endregion
635
1069
  //#region src/federation/handler.ts
636
1070
  const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
637
1071
  const INBOX_COMPOUND_PROOF_LIMITS = {
@@ -729,8 +1163,9 @@ async function handleObject(request, { values, context, objectDispatcher, author
729
1163
  }
730
1164
  /**
731
1165
  * Handles an FEP-ef61 gateway request for a portable object through an object
732
- * dispatcher. The object is served only if its ID canonically matches the
733
- * requested portable ID and it satisfies the FEP-ef61 proof policy.
1166
+ * dispatcher, or the actor dispatcher adapted to one. The object is served
1167
+ * only if its ID canonically matches the requested portable ID and it
1168
+ * satisfies the FEP-ef61 proof policy.
734
1169
  * @template TContextData The context data to pass to the context.
735
1170
  * @param request The HTTP request.
736
1171
  * @param parameters The parameters for handling the portable object.
@@ -1970,7 +2405,21 @@ async function handleInboxInternal(request, parameters, span) {
1970
2405
  inbox: portableInbox.recipient.canonicalInboxId
1971
2406
  });
1972
2407
  else try {
1973
- await portableInbox.forward(json, activity.id, getTypeId(activity).href);
2408
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href, listRequestSignatures(request).map((signature) => ({
2409
+ keyId: signature.keyId,
2410
+ coversDelivery: coversDelivery(signature),
2411
+ verify: async () => {
2412
+ const verification = await verifyRequestDetailed(selectRequestSignature(request, signature), {
2413
+ contextLoader: ctx.contextLoader,
2414
+ documentLoader: ctx.documentLoader,
2415
+ timeWindow: signatureTimeWindow,
2416
+ keyCache,
2417
+ meterProvider,
2418
+ tracerProvider
2419
+ });
2420
+ return verification.verified ? verification.key : null;
2421
+ }
2422
+ })));
1974
2423
  } catch (error) {
1975
2424
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
1976
2425
  activityId: activity.id.href,
@@ -2603,363 +3052,61 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
2603
3052
  async function setKey(kv, key, policy) {
2604
3053
  const seconds = policy.nonceTtlSeconds ?? 300;
2605
3054
  const ttl = Temporal.Duration.from({ seconds });
2606
- await kv.set(key, true, { ttl });
2607
- }
2608
- const DEF_COMPONENTS = [
2609
- "@method",
2610
- "@target-uri",
2611
- "@authority",
2612
- "content-digest"
2613
- ];
2614
- const MIN_COMPONENTS = [
2615
- "@method",
2616
- "@target-uri",
2617
- "@authority"
2618
- ];
2619
- /**
2620
- * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
2621
- * portable actor, i.e., its ID is a compatible identifier and it is owned by
2622
- * a portable actor, which only the portable actor's signed document can
2623
- * establish.
2624
- */
2625
- function isPortableGatewayKey(key) {
2626
- return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
2627
- }
2628
- /**
2629
- * Checks whether an activity needs an Object Integrity Proof by its DID:
2630
- * either it is performed by an [FEP-ef61] portable actor, or its own ID is
2631
- * portable. Both `ap:` URIs and compatible identifiers count as portable.
2632
- *
2633
- * [FEP-ef61]: https://w3id.org/fep/ef61
2634
- */
2635
- function isPortableActivity(activity) {
2636
- return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
2637
- }
2638
- /**
2639
- * Verifies that a portable activity's own ID belongs to the DID that signed
2640
- * it, on the JSON-LD document received. The ID the proof policy checks has
2641
- * to be the parsed activity's ID, so that a remote context that changes
2642
- * between loads cannot make the two name different objects.
2643
- * @returns `null` if the ID is verified, or the reason why it is not.
2644
- */
2645
- async function verifyPortableActivityId(activityId, jsonLd, options) {
2646
- const expectedId = getCanonicalPortableId(activityId);
2647
- if (expectedId == null) return "malformed portable ID";
2648
- let verification;
2649
- try {
2650
- verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
2651
- } catch (error) {
2652
- if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
2653
- if (!isPermanentActivityParseError(error)) throw error;
2654
- return "malformed document";
2655
- }
2656
- const { result, root } = verification;
2657
- if (!result.verified) return result.reason.type;
2658
- const rootId = root?.["@id"];
2659
- if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
2660
- return null;
2661
- }
2662
- //#endregion
2663
- //#region src/federation/portable-inbox.ts
2664
- const MAX_TIMEOUT_MS = 2 ** 31 - 1;
2665
- /**
2666
- * Fills in the defaults of {@link PortableInboxForwardingOptions} and
2667
- * validates them.
2668
- * @param options The options.
2669
- * @returns The resolved options.
2670
- * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
2671
- * is not positive, if `deadline` is negative or longer
2672
- * than 2,147,483,647 milliseconds (about 24.8 days), or
2673
- * if either duration has calendar units, i.e., weeks,
2674
- * months, or years, whose length depends on the date.
2675
- */
2676
- function resolvePortableInboxForwardingOptions(options = {}) {
2677
- const maxTargets = options.maxTargets ?? 10;
2678
- if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
2679
- const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
2680
- if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
2681
- const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
2682
- const deadlineMs = deadline.total("millisecond");
2683
- if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
2684
- return {
2685
- maxTargets,
2686
- ttl,
2687
- deadline
2688
- };
2689
- }
2690
- /**
2691
- * Decides whether this server accepts a delivery to a portable inbox on behalf
2692
- * of the given actor, which the actor dispatcher returned for the identifier
2693
- * in the requested inbox path.
2694
- *
2695
- * The actor document comes from the application, so it is trusted as is and
2696
- * its proof is not verified here.
2697
- * @param actor The actor the actor dispatcher returned.
2698
- * @param options The requested DID, the canonical form of the requested inbox
2699
- * ID, and the origin of this server.
2700
- * @returns The resolution.
2701
- */
2702
- function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
2703
- if (actor == null) return {
2704
- status: "rejected",
2705
- reason: "notFound"
2706
- };
2707
- const id = actor.id;
2708
- const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
2709
- if (id == null || did == null) return {
2710
- status: "rejected",
2711
- reason: "notPortable"
2712
- };
2713
- if (did !== authority) return {
2714
- status: "rejected",
2715
- reason: "authorityMismatch"
2716
- };
2717
- const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
2718
- if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
2719
- status: "rejected",
2720
- reason: "inboxMismatch"
2721
- };
2722
- const gateways = actor.gateways;
2723
- if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
2724
- status: "rejected",
2725
- reason: "notGateway"
2726
- };
2727
- return {
2728
- status: "accepted",
2729
- recipient: {
2730
- actorId: id,
2731
- inboxId,
2732
- canonicalInboxId,
2733
- gateways
2734
- }
2735
- };
3055
+ await kv.set(key, true, { ttl });
2736
3056
  }
3057
+ const DEF_COMPONENTS = [
3058
+ "@method",
3059
+ "@target-uri",
3060
+ "@authority",
3061
+ "content-digest"
3062
+ ];
3063
+ const MIN_COMPONENTS = [
3064
+ "@method",
3065
+ "@target-uri",
3066
+ "@authority"
3067
+ ];
2737
3068
  /**
2738
- * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
2739
- * identifier, keeping its query.
2740
- * @returns The portable URI, or `null` if the ID is neither, or is malformed.
3069
+ * Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
3070
+ * portable actor, i.e., its ID is a compatible identifier and it is owned by
3071
+ * a portable actor, which only the portable actor's signed document can
3072
+ * establish.
2741
3073
  */
2742
- function toPortableUri(id) {
2743
- if (isPortableUri(id)) return id;
2744
- try {
2745
- return fromCompatibleEf61Id(id);
2746
- } catch (error) {
2747
- if (error instanceof TypeError) return null;
2748
- throw error;
2749
- }
3074
+ function isPortableGatewayKey(key) {
3075
+ return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
2750
3076
  }
2751
3077
  /**
2752
- * Forwards an activity received in a portable inbox to the inboxes of the same
2753
- * portable actor on its other gateways, as FEP-ef61 recommends.
2754
- *
2755
- * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
2756
- * each target gateway is claimed with an atomic compare-and-swap before the
2757
- * activity is handed off, and a claim is never released, even if the hand-off
2758
- * fails, since a failed hand-off does not prove that nothing was delivered.
2759
- * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
2760
- * forwarded.
3078
+ * Checks whether an activity needs an Object Integrity Proof by its DID:
3079
+ * either it is performed by an [FEP-ef61] portable actor, or its own ID is
3080
+ * portable. Both `ap:` URIs and compatible identifiers count as portable.
2761
3081
  *
2762
- * Forwarded requests are signed with HTTP Signatures by the RSA key among
2763
- * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
2764
- * servers requiring HTTP Signatures accept them, and are sent unsigned
2765
- * otherwise. Either way, the receiving gateways authenticate the activity by
2766
- * its own proof.
2767
- * @param parameters The parameters.
2768
- * @returns The target inbox URLs that the activity was handed off to.
3082
+ * [FEP-ef61]: https://w3id.org/fep/ef61
2769
3083
  */
2770
- async function forwardPortableInboxActivity(parameters) {
2771
- const logger = getLogger([
2772
- "fedify",
2773
- "federation",
2774
- "inbox"
2775
- ]);
2776
- const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
2777
- const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
2778
- if (kv.cas == null || maxTargets < 1) return [];
2779
- const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
2780
- const targets = getForwardingTargets(recipient, excludedOrigins);
2781
- if (targets.length > maxTargets) {
2782
- 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.", {
2783
- inbox: recipient.canonicalInboxId,
2784
- activityId: activityId.href,
2785
- max: maxTargets
2786
- });
2787
- targets.length = maxTargets;
2788
- }
2789
- const claimed = [];
2790
- for (const { gateway, inbox } of targets) {
2791
- const key = [
2792
- ...kvPrefix,
2793
- recipient.canonicalInboxId,
2794
- canonicalActivityId,
2795
- gateway
2796
- ];
2797
- if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
2798
- }
2799
- if (claimed.length < 1) return [];
2800
- logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
2801
- activityId: activityId.href,
2802
- inbox: recipient.canonicalInboxId,
2803
- targets: claimed.map((t) => t.href)
2804
- });
2805
- if (outboxQueue == null) await forwardImmediately(parameters, claimed);
2806
- else await enqueueForwarding(parameters, outboxQueue, claimed);
2807
- return claimed;
2808
- }
2809
- function getForwardingTargets(recipient, excludedOrigins) {
2810
- const logger = getLogger([
2811
- "fedify",
2812
- "federation",
2813
- "inbox"
2814
- ]);
2815
- const seen = new Set(excludedOrigins);
2816
- const targets = [];
2817
- for (const gateway of recipient.gateways) {
2818
- if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
2819
- seen.add(gateway.origin);
2820
- let inbox;
2821
- try {
2822
- inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
2823
- } catch (error) {
2824
- if (!(error instanceof TypeError)) throw error;
2825
- logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
2826
- gateway: gateway.origin,
2827
- inbox: recipient.canonicalInboxId
2828
- });
2829
- continue;
2830
- }
2831
- targets.push({
2832
- gateway: gateway.origin,
2833
- inbox
2834
- });
2835
- }
2836
- return targets;
3084
+ function isPortableActivity(activity) {
3085
+ return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
2837
3086
  }
2838
3087
  /**
2839
- * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
2840
- * key, as {@link sendActivity} signs requests only with such a key. Since
2841
- * the targets have already been claimed, the activity is forwarded unsigned
2842
- * rather than not at all if the key cannot be resolved or used.
3088
+ * Verifies that a portable activity's own ID belongs to the DID that signed
3089
+ * it, on the JSON-LD document received. The ID the proof policy checks has
3090
+ * to be the parsed activity's ID, so that a remote context that changes
3091
+ * between loads cannot make the two name different objects.
3092
+ * @returns `null` if the ID is verified, or the reason why it is not.
2843
3093
  */
2844
- async function resolveForwardingKey({ getKeys, activityId }) {
2845
- if (getKeys == null) return null;
2846
- const logger = getLogger([
2847
- "fedify",
2848
- "federation",
2849
- "inbox"
2850
- ]);
2851
- let key;
2852
- try {
2853
- key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
2854
- } catch (error) {
2855
- logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
2856
- activityId: activityId.href,
2857
- error
2858
- });
2859
- return null;
2860
- }
2861
- if (key == null) return null;
3094
+ async function verifyPortableActivityId(activityId, jsonLd, options) {
3095
+ const expectedId = getCanonicalPortableId(activityId);
3096
+ if (expectedId == null) return "malformed portable ID";
3097
+ let verification;
2862
3098
  try {
2863
- validateCryptoKey(key.privateKey, "private");
2864
- } catch (error) {
2865
- if (!(error instanceof TypeError)) throw error;
2866
- logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
2867
- keyId: key.keyId.href,
2868
- activityId: activityId.href,
2869
- error
2870
- });
2871
- return null;
2872
- }
2873
- return key;
2874
- }
2875
- async function forwardImmediately(parameters, inboxes) {
2876
- const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
2877
- const logger = getLogger([
2878
- "fedify",
2879
- "federation",
2880
- "inbox"
2881
- ]);
2882
- const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
2883
- const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
2884
- activity,
2885
- activityId: activityId.href,
2886
- activityType,
2887
- keys: key == null ? [] : [key],
2888
- inbox,
2889
- allowPrivateAddress,
2890
- specDeterminer,
2891
- meterProvider,
2892
- tracerProvider
2893
- }).catch((error) => {
2894
- logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2895
- activityId: activityId.href,
2896
- inbox: inbox.href,
2897
- error
2898
- });
2899
- }))));
2900
- let timer;
2901
- const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
2902
- timer = setTimeout(() => resolve(true), deadline);
2903
- })]);
2904
- clearTimeout(timer);
2905
- if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
2906
- activityId: activityId.href,
2907
- deadline
2908
- });
2909
- }
2910
- async function enqueueForwarding(parameters, outboxQueue, inboxes) {
2911
- const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
2912
- const logger = getLogger([
2913
- "fedify",
2914
- "federation",
2915
- "inbox"
2916
- ]);
2917
- const key = await resolveForwardingKey(parameters);
2918
- const keys = [];
2919
- if (key != null) try {
2920
- keys.push({
2921
- keyId: key.keyId.href,
2922
- privateKey: await exportJwk(key.privateKey)
2923
- });
3099
+ verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
2924
3100
  } catch (error) {
2925
- logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
2926
- keyId: key.keyId.href,
2927
- activityId: activityId.href,
2928
- error
2929
- });
3101
+ if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
3102
+ if (!isPermanentActivityParseError(error)) throw error;
3103
+ return "malformed document";
2930
3104
  }
2931
- startQueue?.();
2932
- const started = (/* @__PURE__ */ new Date()).toISOString();
2933
- const traceContext = {};
2934
- propagation.inject(context.active(), traceContext);
2935
- await Promise.all(inboxes.map(async (inbox) => {
2936
- const message = {
2937
- type: "outbox",
2938
- id: crypto.randomUUID(),
2939
- baseUrl,
2940
- keys,
2941
- activity,
2942
- activityId: activityId.href,
2943
- activityType,
2944
- inbox: inbox.href,
2945
- sharedInbox: false,
2946
- started,
2947
- attempt: 0,
2948
- headers: {},
2949
- traceContext
2950
- };
2951
- try {
2952
- await outboxQueue.enqueue(message);
2953
- } catch (error) {
2954
- logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
2955
- activityId: activityId.href,
2956
- inbox: inbox.href,
2957
- error
2958
- });
2959
- return;
2960
- }
2961
- recordOutboxEnqueue(meterProvider, outboxQueue, message);
2962
- }));
3105
+ const { result, root } = verification;
3106
+ if (!result.verified) return result.reason.type;
3107
+ const rootId = root?.["@id"];
3108
+ if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
3109
+ return null;
2963
3110
  }
2964
3111
  //#endregion
2965
3112
  //#region src/federation/outgoing-proof.ts
@@ -5348,17 +5495,39 @@ var FederationImpl = class extends FederationBuilderImpl {
5348
5495
  });
5349
5496
  }
5350
5497
  const route = this.router.route(portable.path);
5351
- if (route == null || !route.name.startsWith("object:")) {
5498
+ const isActor = route != null && (route.name === "actor" || route.name.startsWith("actorAlias:"));
5499
+ if (route == null || !isActor && !route.name.startsWith("object:")) {
5352
5500
  metricState.endpoint = "not_found";
5353
5501
  return await onNotFound(request);
5354
5502
  }
5355
5503
  metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
5356
- metricState.endpoint = "object";
5504
+ metricState.endpoint = isActor ? "actor" : "object";
5357
5505
  span.updateName(`${request.method} ${metricState.routeTemplate}`);
5358
5506
  if (!acceptsJsonLd(request)) {
5359
5507
  metricState.endpoint = "not_acceptable";
5360
5508
  return await onNotAcceptable(request);
5361
5509
  }
5510
+ if (isActor) {
5511
+ const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
5512
+ const context = this.#createContext(request, contextData, {
5513
+ invokedFromActorDispatcher: { identifier },
5514
+ portableRequest: portable.portableRequest
5515
+ });
5516
+ const actorDispatcher = this.actorCallbacks?.dispatcher;
5517
+ const authorizePredicate = this.actorCallbacks?.authorizePredicate;
5518
+ return await handlePortableObject(request, {
5519
+ values: { identifier },
5520
+ context,
5521
+ objectDispatcher: actorDispatcher == null ? void 0 : async (ctx) => {
5522
+ const actor = await actorDispatcher(ctx, identifier);
5523
+ return actor instanceof Tombstone ? null : actor;
5524
+ },
5525
+ authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
5526
+ canonicalId: portable.canonicalId,
5527
+ onUnauthorized,
5528
+ onNotFound
5529
+ });
5530
+ }
5362
5531
  const typeId = route.name.replace(/^object:/, "");
5363
5532
  const callbacks = this.objectCallbacks[typeId];
5364
5533
  const cls = this.objectTypeIds[typeId];
@@ -5457,7 +5626,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5457
5626
  onNotFound,
5458
5627
  portableInbox: {
5459
5628
  recipient,
5460
- forward: async (activity, activityId, activityType) => {
5629
+ forward: async (activity, activityId, activityType, signatures) => {
5461
5630
  if (this.portableInboxForwarding.maxTargets < 1) return;
5462
5631
  if (this.kv.cas == null) {
5463
5632
  if (!this.#portableInboxForwardingWarned) {
@@ -5480,6 +5649,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5480
5649
  allowPrivateAddress: this.allowPrivateAddress,
5481
5650
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
5482
5651
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
5652
+ signatures,
5483
5653
  options: this.portableInboxForwarding,
5484
5654
  meterProvider: this.meterProvider,
5485
5655
  tracerProvider: this.tracerProvider
@@ -5721,9 +5891,17 @@ var ContextImpl = class ContextImpl {
5721
5891
  return new URL(path, this.canonicalOrigin);
5722
5892
  }
5723
5893
  getActorUri(identifier) {
5894
+ return new URL(this.#getActorPath(identifier), this.canonicalOrigin);
5895
+ }
5896
+ getPortableActorUri(identifier, authority) {
5897
+ const path = this.#getActorPath(identifier);
5898
+ if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
5899
+ return buildPortableUri(authority, path);
5900
+ }
5901
+ #getActorPath(identifier) {
5724
5902
  const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
5725
5903
  if (path == null) throw new RouterError("No actor dispatcher registered.");
5726
- return new URL(path, this.canonicalOrigin);
5904
+ return path;
5727
5905
  }
5728
5906
  getObjectUri(cls, values) {
5729
5907
  return new URL(this.#getObjectPath(cls, values), this.canonicalOrigin);
@@ -6366,6 +6544,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
6366
6544
  request: this.request
6367
6545
  });
6368
6546
  }
6547
+ getPortableActorUri(identifier, authority) {
6548
+ return super.getPortableActorUri(identifier, authority ?? this.portableRequest?.authority);
6549
+ }
6369
6550
  getPortableObjectUri(cls, values, authority) {
6370
6551
  return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
6371
6552
  }
@@ -6762,4 +6943,4 @@ function getRequestId(request) {
6762
6943
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
6763
6944
  }
6764
6945
  //#endregion
6765
- export { handleNodeInfoJrd as C, handleNodeInfo as S, autoIdAssigner as T, respondWithObject as _, OutboxContextImpl as a, parseHashlinkGatewayRequest as b, assertPortableActorActivity as c, handleActor as d, handleCollection as f, handleOutbox as g, handleObject as h, KvSpecDeterminer as i, forwardPortableInboxActivity as l, handleInbox as m, FederationImpl as n, createFederation as o, handleCustomCollection as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, resolvePortableInboxForwardingOptions as u, respondWithObjectIfAcceptable as v, actorDehydrator as w, handleBenchmarkTrigger as x, PORTABLE_OBJECT_CONTENT_TYPE as y };
6946
+ export { handleNodeInfo as C, autoIdAssigner as E, handleBenchmarkTrigger as S, actorDehydrator as T, coversDelivery as _, OutboxContextImpl as a, PORTABLE_OBJECT_CONTENT_TYPE as b, assertPortableActorActivity as c, handleCustomCollection as d, handleInbox as f, respondWithObjectIfAcceptable as g, respondWithObject as h, KvSpecDeterminer as i, handleActor as l, handleOutbox as m, FederationImpl as n, createFederation as o, handleObject as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCollection as u, forwardPortableInboxActivity as v, handleNodeInfoJrd as w, parseHashlinkGatewayRequest as x, resolvePortableInboxForwardingOptions as y };