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

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 (79) hide show
  1. package/dist/{builder-Ld4Y9ymR.mjs → builder-Bno54o_-.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-X3SxXVbc.mjs} +2 -2
  6. package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
  7. package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
  8. package/dist/{deno-8t8k_6Yv.mjs → deno-dxdvl2XZ.mjs} +1 -1
  9. package/dist/{docloader-DJKcsAiF.mjs → docloader-eBxQDsk9.mjs} +2 -2
  10. package/dist/federation/builder.test.mjs +1 -1
  11. package/dist/federation/compound-fanout.test.mjs +3 -3
  12. package/dist/federation/handler-compound-proof.test.mjs +3 -3
  13. package/dist/federation/handler.test.mjs +5 -5
  14. package/dist/federation/hashlink-media.test.mjs +2 -2
  15. package/dist/federation/idempotency.test.mjs +2 -2
  16. package/dist/federation/metrics.test.mjs +1 -1
  17. package/dist/federation/middleware.test.mjs +7 -7
  18. package/dist/federation/mod.cjs +1 -1
  19. package/dist/federation/mod.d.cts +2 -2
  20. package/dist/federation/mod.d.ts +2 -2
  21. package/dist/federation/mod.js +1 -1
  22. package/dist/federation/outgoing-proof.test.mjs +5 -5
  23. package/dist/federation/portable-gateway-keys.test.mjs +3 -3
  24. package/dist/federation/portable-inbox.test.mjs +375 -10
  25. package/dist/federation/portable.test.mjs +3 -3
  26. package/dist/federation/send.test.mjs +3 -3
  27. package/dist/federation/tasks/enqueue.test.mjs +1 -1
  28. package/dist/federation/tasks/tasks.test.mjs +2 -2
  29. package/dist/federation/temporal.test.mjs +1 -1
  30. package/dist/federation/webfinger.test.mjs +1 -1
  31. package/dist/{federation-aSHO04ob.cjs → federation-C4Ef3JKa.cjs} +1211 -1089
  32. package/dist/{http-DSaSAN7X.cjs → http-C8cZTE3Y.cjs} +89 -2
  33. package/dist/{http-DyT7DP1Y.js → http-D-rbr_Sy.js} +78 -3
  34. package/dist/{http-B2ftY0Ha.mjs → http-DsgawTd8.mjs} +82 -7
  35. package/dist/{key-Dwm4VswT.mjs → key-DYfmjnmp.mjs} +2 -2
  36. package/dist/{kv-cache-DmroAz3M.js → kv-cache-DIxwMeNi.js} +1 -1
  37. package/dist/{kv-cache-BSJAlP4C.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
  38. package/dist/{kv-cache-D0yYukqk.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
  39. package/dist/{ld-BYZOoDzZ.mjs → ld-B00wWHzc.mjs} +3 -3
  40. package/dist/{metrics-B7YwKTFr.mjs → metrics-BP3vdBpb.mjs} +1 -1
  41. package/dist/{middleware-E_0IH72m.mjs → middleware-Br1_d-Ga.mjs} +627 -505
  42. package/dist/{middleware-Blq0882d.js → middleware-CcXMe400.js} +1307 -1185
  43. package/dist/{middleware-BNeqKSsL.mjs → middleware-CeBVBa61.mjs} +1 -1
  44. package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
  45. package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
  46. package/dist/mod.cjs +5 -5
  47. package/dist/mod.d.cts +2 -2
  48. package/dist/mod.d.ts +2 -2
  49. package/dist/mod.js +5 -5
  50. package/dist/nodeinfo/handler.test.mjs +1 -1
  51. package/dist/{owner-Cgx2Dc7I.mjs → owner-BJPDRDRq.mjs} +3 -3
  52. package/dist/{owner-szIZl4qI.cjs → owner-DSzH7wro.cjs} +1 -1
  53. package/dist/{owner-DAGSC5gX.js → owner-U12fDpQW.js} +1 -1
  54. package/dist/{portable-key-Cn1WJSN1.mjs → portable-key-D0mWbV9G.mjs} +1 -1
  55. package/dist/{proof-DVXC6nYz.mjs → proof-ChjBwnep.mjs} +4 -4
  56. package/dist/{send-D-hOvYKy.mjs → send-yFX0E-ye.mjs} +3 -3
  57. package/dist/sig/compound-proof-discovery.test.mjs +1 -1
  58. package/dist/sig/compound-proof-producer.test.mjs +1 -1
  59. package/dist/sig/compound-proof-verification.test.mjs +2 -2
  60. package/dist/sig/compound-proof.test.mjs +2 -2
  61. package/dist/sig/http.test.mjs +72 -2
  62. package/dist/sig/key.test.mjs +1 -1
  63. package/dist/sig/ld.test.mjs +2 -2
  64. package/dist/sig/mod.cjs +3 -3
  65. package/dist/sig/mod.js +3 -3
  66. package/dist/sig/owner.test.mjs +1 -1
  67. package/dist/sig/portable-collection.test.mjs +2 -2
  68. package/dist/sig/portable-key.test.mjs +3 -3
  69. package/dist/sig/proof.test.mjs +3 -3
  70. package/dist/{sig-CXOKN4Br.cjs → sig-B689Nj7y.cjs} +2 -2
  71. package/dist/{sig-B5Ell7pO.js → sig-C85KqAcx.js} +2 -2
  72. package/dist/{temporal-CqpKGEPk.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
  73. package/dist/testing/kv.test.mjs +1 -1
  74. package/dist/testing/mod.d.mts +5 -2
  75. package/dist/utils/docloader.test.mjs +2 -2
  76. package/dist/utils/kv-cache.test.mjs +1 -1
  77. package/dist/utils/mod.cjs +1 -1
  78. package/dist/utils/mod.js +1 -1
  79. package/package.json +6 -6
@@ -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-dxdvl2XZ.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-BP3vdBpb.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-DYfmjnmp.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-B00wWHzc.mjs";
11
+ import { i as signObject, l as verifyPortableObjectProofWithRoot, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, t as InvalidPortableObjectIdError } from "./proof-ChjBwnep.mjs";
12
+ import { a as listRequestSignatures, c as selectRequestSignature, d as verifyRequest, f as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-DsgawTd8.mjs";
13
+ import { t as getAuthenticatedDocumentLoader } from "./docloader-eBxQDsk9.mjs";
14
+ import { n as kvCache } from "./kv-cache-EvOgrFsS.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-BJPDRDRq.mjs";
18
+ import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-yFX0E-ye.mjs";
19
+ import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Bno54o_-.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-X3SxXVbc.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-Dl9x-Qw4.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";
@@ -632,234 +632,643 @@ function parseHashlinkGatewayRequest(url) {
632
632
  };
633
633
  }
634
634
  //#endregion
635
- //#region src/federation/handler.ts
636
- const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
637
- const INBOX_COMPOUND_PROOF_LIMITS = {
638
- maxDepth: 64,
639
- maxMaps: 1e4,
640
- maxProofs: 32,
641
- maxBytes: 10 * 1024 * 1024
642
- };
643
- function isRemoteContextLoadingFailure$1(error) {
644
- return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
645
- }
646
- function isPermanentRemoteContextError$1(error) {
647
- if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
648
- const details = error.details;
649
- if (details?.code === "invalid remote context") return true;
650
- return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
651
- }
652
- function isInvalidJsonLdError(error) {
653
- if (!(error instanceof Error)) return false;
654
- const name = error.name;
655
- return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
656
- }
657
- function isValidationTypeError(error) {
658
- return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
635
+ //#region src/federation/portable-inbox.ts
636
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
637
+ /**
638
+ * Fills in the defaults of {@link PortableInboxForwardingOptions} and
639
+ * validates them.
640
+ * @param options The options.
641
+ * @returns The resolved options.
642
+ * @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
643
+ * is not positive, if `deadline` is negative or longer
644
+ * than 2,147,483,647 milliseconds (about 24.8 days), or
645
+ * if either duration has calendar units, i.e., weeks,
646
+ * months, or years, whose length depends on the date.
647
+ */
648
+ function resolvePortableInboxForwardingOptions(options = {}) {
649
+ const maxTargets = options.maxTargets ?? 10;
650
+ if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
651
+ const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
652
+ if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
653
+ const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
654
+ const deadlineMs = deadline.total("millisecond");
655
+ if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
656
+ return {
657
+ maxTargets,
658
+ ttl,
659
+ deadline
660
+ };
659
661
  }
660
- function isPermanentActivityParseError(error) {
661
- return isInvalidJsonLdError(error) || isValidationTypeError(error);
662
+ /**
663
+ * Decides whether this server accepts a delivery to a portable inbox on behalf
664
+ * of the given actor, which the actor dispatcher returned for the identifier
665
+ * in the requested inbox path.
666
+ *
667
+ * The actor document comes from the application, so it is trusted as is and
668
+ * its proof is not verified here.
669
+ * @param actor The actor the actor dispatcher returned.
670
+ * @param options The requested DID, the canonical form of the requested inbox
671
+ * ID, and the origin of this server.
672
+ * @returns The resolution.
673
+ */
674
+ function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
675
+ if (actor == null) return {
676
+ status: "rejected",
677
+ reason: "notFound"
678
+ };
679
+ const id = actor.id;
680
+ const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
681
+ if (id == null || did == null) return {
682
+ status: "rejected",
683
+ reason: "notPortable"
684
+ };
685
+ if (did !== authority) return {
686
+ status: "rejected",
687
+ reason: "authorityMismatch"
688
+ };
689
+ const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
690
+ if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
691
+ status: "rejected",
692
+ reason: "inboxMismatch"
693
+ };
694
+ const gateways = actor.gateways;
695
+ if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
696
+ status: "rejected",
697
+ reason: "notGateway"
698
+ };
699
+ return {
700
+ status: "accepted",
701
+ recipient: {
702
+ actorId: id,
703
+ inboxId,
704
+ canonicalInboxId,
705
+ gateways
706
+ }
707
+ };
662
708
  }
663
- function hasHttpSignatureHeaders(request) {
664
- return request.headers.has("Signature") || request.headers.has("Signature-Input");
709
+ /**
710
+ * Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
711
+ * identifier, keeping its query.
712
+ * @returns The portable URI, or `null` if the ID is neither, or is malformed.
713
+ */
714
+ function toPortableUri(id) {
715
+ if (isPortableUri(id)) return id;
716
+ try {
717
+ return fromCompatibleEf61Id(id);
718
+ } catch (error) {
719
+ if (error instanceof TypeError) return null;
720
+ throw error;
721
+ }
665
722
  }
666
- function hasObjectIntegrityProof(json) {
667
- return typeof json === "object" && json != null && "proof" in json;
723
+ const RFC9421_DELIVERY_COMPONENTS = [
724
+ "@method",
725
+ "@target-uri",
726
+ "content-digest"
727
+ ];
728
+ const DRAFT_DELIVERY_COMPONENTS = [
729
+ "(request-target)",
730
+ "host",
731
+ "digest"
732
+ ];
733
+ /**
734
+ * Checks whether an HTTP Signature covers the method, the target URI, and the
735
+ * body of a request, which both Fedify and Mitra sign when forwarding.
736
+ * @param signature The signature.
737
+ * @returns `true` if the signature covers them.
738
+ */
739
+ function coversDelivery(signature) {
740
+ 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));
668
741
  }
669
742
  /**
670
- * Handles an actor request.
671
- * @template TContextData The context data to pass to the context.
672
- * @param request The HTTP request.
673
- * @param parameters The parameters for handling the actor.
674
- * @returns A promise that resolves to an HTTP response.
743
+ * Forwards an activity received in a portable inbox to the inboxes of the same
744
+ * portable actor on its other gateways, as FEP-ef61 recommends.
745
+ *
746
+ * FEP-ef61 forbids forwarding an activity from an inbox more than once, so
747
+ * each target gateway is claimed with an atomic compare-and-swap before the
748
+ * activity is handed off, and a claim is never released, even if the hand-off
749
+ * fails, since a failed hand-off does not prove that nothing was delivered.
750
+ * Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
751
+ * forwarded.
752
+ *
753
+ * Forwarded requests are signed with HTTP Signatures by the RSA key among
754
+ * {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
755
+ * servers requiring HTTP Signatures accept them, and are sent unsigned
756
+ * otherwise. Either way, the receiving gateways authenticate the activity by
757
+ * its own proof.
758
+ *
759
+ * If the delivery is signed with a gateway key of the recipient by one of its
760
+ * other gateways, that gateway has the activity already, so it is claimed
761
+ * without forwarding the activity to it. See {@link identifySendingGateway}.
762
+ * @param parameters The parameters.
763
+ * @returns The target inbox URLs that the activity was handed off to.
675
764
  */
676
- async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
765
+ async function forwardPortableInboxActivity(parameters) {
677
766
  const logger = getLogger([
678
767
  "fedify",
679
768
  "federation",
680
- "actor"
769
+ "inbox"
681
770
  ]);
682
- if (actorDispatcher == null) {
683
- logger.debug("Actor dispatcher is not set.", { identifier });
684
- return await onNotFound(request);
685
- }
686
- const actor = await actorDispatcher(context, identifier);
687
- if (actor == null) {
688
- logger.debug("Actor {identifier} not found.", { identifier });
689
- return await onNotFound(request);
690
- }
691
- if (authorizePredicate != null) {
692
- if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
693
- }
694
- if (actor instanceof Tombstone) {
695
- const jsonLd = await actor.toJsonLd(context);
696
- return new Response(JSON.stringify(jsonLd), {
697
- status: 410,
698
- headers: {
699
- "Content-Type": "application/activity+json",
700
- Vary: "Accept"
701
- }
771
+ const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
772
+ const options = parameters.options ?? resolvePortableInboxForwardingOptions();
773
+ const { maxTargets, ttl } = options;
774
+ if (kv.cas == null || maxTargets < 1) return [];
775
+ const deadline = Date.now() + options.deadline.total("millisecond");
776
+ const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
777
+ const claimKey = (gateway) => [
778
+ ...kvPrefix,
779
+ recipient.canonicalInboxId,
780
+ canonicalActivityId,
781
+ gateway
782
+ ];
783
+ let targets = getForwardingTargets(recipient, excludedOrigins);
784
+ const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
785
+ if (sender != null) {
786
+ logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
787
+ activityId: activityId.href,
788
+ inbox: recipient.canonicalInboxId,
789
+ gateway: sender
702
790
  });
791
+ await kv.cas(claimKey(sender), void 0, true, { ttl });
792
+ targets = targets.filter((t) => t.gateway !== sender);
703
793
  }
704
- const jsonLd = await actor.toJsonLd(context);
705
- return new Response(JSON.stringify(jsonLd), { headers: {
706
- "Content-Type": "application/activity+json",
707
- Vary: "Accept"
708
- } });
709
- }
710
- /**
711
- * Handles an object request.
712
- * @template TContextData The context data to pass to the context.
713
- * @param request The HTTP request.
714
- * @param parameters The parameters for handling the object.
715
- * @returns A promise that resolves to an HTTP response.
716
- */
717
- async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
718
- if (objectDispatcher == null) return await onNotFound(request);
719
- const object = await objectDispatcher(context, values);
720
- if (object == null) return await onNotFound(request);
721
- if (authorizePredicate != null) {
722
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
794
+ if (targets.length > maxTargets) {
795
+ 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.", {
796
+ inbox: recipient.canonicalInboxId,
797
+ activityId: activityId.href,
798
+ max: maxTargets
799
+ });
800
+ targets.length = maxTargets;
723
801
  }
724
- const jsonLd = await object.toJsonLd(context);
725
- return new Response(JSON.stringify(jsonLd), { headers: {
726
- "Content-Type": "application/activity+json",
727
- Vary: "Accept"
728
- } });
802
+ const claimed = [];
803
+ for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
804
+ if (claimed.length < 1) return [];
805
+ logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
806
+ activityId: activityId.href,
807
+ inbox: recipient.canonicalInboxId,
808
+ targets: claimed.map((t) => t.href)
809
+ });
810
+ if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
811
+ else await enqueueForwarding(parameters, outboxQueue, claimed);
812
+ return claimed;
729
813
  }
730
814
  /**
731
- * 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.
734
- * @template TContextData The context data to pass to the context.
735
- * @param request The HTTP request.
736
- * @param parameters The parameters for handling the portable object.
737
- * @returns A promise that resolves to an HTTP response.
815
+ * Identifies the gateway of the recipient that forwarded a delivery to its
816
+ * portable inbox, by the delivery's HTTP Signature made with the recipient's
817
+ * gateway key, which FEP-ef61 has each gateway use for requests on behalf of
818
+ * an actor. Since the activity is authenticated by its own proof, the
819
+ * signature only tells which gateway to skip, and a delivery whose signature
820
+ * is invalid or cannot be verified is still forwarded to every gateway.
821
+ *
822
+ * Verifying the signature fetches the recipient's actor document from the
823
+ * sending gateway, as gateway keys are not cached, so only the first signature
824
+ * that may save a request is verified: one that covers the delivery and names
825
+ * a gateway key of the recipient on one of the forwarding targets that the
826
+ * activity has not been forwarded to yet.
827
+ * @param parameters The forwarding parameters.
828
+ * @param targets The origins of the gateways to forward to.
829
+ * @param claimKey Gets the key of the claim for a gateway.
830
+ * @param deadline The time by which identification has to finish, in
831
+ * milliseconds since the epoch; afterwards, the sending
832
+ * gateway is left unidentified.
833
+ * @returns The origin of the sending gateway, or `null` if it is not
834
+ * identified.
738
835
  */
739
- async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
836
+ async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
837
+ if (signatures == null || signatures.length < 1) return null;
740
838
  const logger = getLogger([
741
839
  "fedify",
742
840
  "federation",
743
- "object"
841
+ "inbox"
744
842
  ]);
745
- if (objectDispatcher == null) return await onNotFound(request);
746
- const object = await objectDispatcher(context, values);
747
- if (object == null) return await onNotFound(request);
748
- if (!isRequestedPortableObject(object, canonicalId)) {
749
- logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
750
- objectId: object.id?.href,
751
- portableId: canonicalId
752
- });
753
- return await onNotFound(request);
754
- }
755
- if (authorizePredicate != null) {
756
- if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
843
+ const actorId = getCanonicalPortableId(recipient.actorId);
844
+ if (actorId == null) return null;
845
+ const isGatewayKeyOfRecipient = (keyId) => {
846
+ if (!isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
847
+ const base = new URL(keyId.href);
848
+ base.hash = "";
849
+ return getCanonicalPortableId(base) === actorId;
850
+ };
851
+ let signature;
852
+ for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
853
+ signature = candidate;
854
+ break;
757
855
  }
758
- let jsonLd;
759
- let servedId;
760
- let result;
761
- try {
762
- jsonLd = await object.toJsonLd(context);
763
- servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
764
- result = await verifyPortableObjectProof(jsonLd, {
765
- contextLoader: context.contextLoader,
766
- documentLoader: context.documentLoader,
767
- tracerProvider: context.tracerProvider,
768
- meterProvider: context.meterProvider
856
+ if (signature == null) return null;
857
+ const { keyId } = signature;
858
+ const logProperties = {
859
+ activityId: activityId.href,
860
+ inbox: recipient.canonicalInboxId,
861
+ keyId: keyId.href
862
+ };
863
+ const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
864
+ 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);
865
+ return key;
866
+ }, (error) => {
867
+ 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}", {
868
+ ...logProperties,
869
+ error
769
870
  });
770
- } catch (error) {
771
- logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
772
- portableId: canonicalId,
773
- error
871
+ return null;
872
+ });
873
+ let timer;
874
+ const key = await Promise.race([verification, new Promise((resolve) => {
875
+ timer = setTimeout(() => {
876
+ 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);
877
+ resolve(null);
878
+ }, Math.max(0, deadline - Date.now()));
879
+ })]);
880
+ clearTimeout(timer);
881
+ if (key == null) return null;
882
+ if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !isPortableId(key.ownerId) || getCanonicalPortableId(key.ownerId) !== actorId) {
883
+ 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);
884
+ return null;
885
+ }
886
+ return key.id.origin;
887
+ }
888
+ function getForwardingTargets(recipient, excludedOrigins) {
889
+ const logger = getLogger([
890
+ "fedify",
891
+ "federation",
892
+ "inbox"
893
+ ]);
894
+ const seen = new Set(excludedOrigins);
895
+ const targets = [];
896
+ for (const gateway of recipient.gateways) {
897
+ if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
898
+ seen.add(gateway.origin);
899
+ let inbox;
900
+ try {
901
+ inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
902
+ } catch (error) {
903
+ if (!(error instanceof TypeError)) throw error;
904
+ logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
905
+ gateway: gateway.origin,
906
+ inbox: recipient.canonicalInboxId
907
+ });
908
+ continue;
909
+ }
910
+ targets.push({
911
+ gateway: gateway.origin,
912
+ inbox
774
913
  });
775
- return portableObjectInternalServerError(request);
776
914
  }
777
- if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
778
- logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
779
- return await onNotFound(request);
915
+ return targets;
916
+ }
917
+ /**
918
+ * Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
919
+ * key, as {@link sendActivity} signs requests only with such a key. Since
920
+ * the targets have already been claimed, the activity is forwarded unsigned
921
+ * rather than not at all if the key cannot be resolved or used.
922
+ */
923
+ async function resolveForwardingKey({ getKeys, activityId }) {
924
+ if (getKeys == null) return null;
925
+ const logger = getLogger([
926
+ "fedify",
927
+ "federation",
928
+ "inbox"
929
+ ]);
930
+ let key;
931
+ try {
932
+ key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
933
+ } catch (error) {
934
+ logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
935
+ activityId: activityId.href,
936
+ error
937
+ });
938
+ return null;
780
939
  }
781
- if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
782
- logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
783
- portableId: canonicalId,
784
- reason: result.reason.type
940
+ if (key == null) return null;
941
+ try {
942
+ validateCryptoKey(key.privateKey, "private");
943
+ } catch (error) {
944
+ if (!(error instanceof TypeError)) throw error;
945
+ logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
946
+ keyId: key.keyId.href,
947
+ activityId: activityId.href,
948
+ error
785
949
  });
786
- return portableObjectInternalServerError(request);
950
+ return null;
787
951
  }
788
- return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
789
- "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
790
- Vary: "Accept"
791
- } });
792
- }
793
- function isRequestedPortableObject(object, canonicalId) {
794
- return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
952
+ return key;
795
953
  }
796
- async function getJsonLdRootId(jsonLd, contextLoader) {
797
- const expanded = await jsonld.expand(jsonLd, {
798
- documentLoader: getNormalizationContextLoader(contextLoader),
799
- keepFreeFloatingNodes: true
954
+ async function forwardImmediately(parameters, inboxes, deadlineAt) {
955
+ const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
956
+ const logger = getLogger([
957
+ "fedify",
958
+ "federation",
959
+ "inbox"
960
+ ]);
961
+ const deadline = Math.max(0, deadlineAt - Date.now());
962
+ const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
963
+ activity,
964
+ activityId: activityId.href,
965
+ activityType,
966
+ keys: key == null ? [] : [key],
967
+ inbox,
968
+ allowPrivateAddress,
969
+ specDeterminer,
970
+ meterProvider,
971
+ tracerProvider
972
+ }).catch((error) => {
973
+ logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
974
+ activityId: activityId.href,
975
+ inbox: inbox.href,
976
+ error
977
+ });
978
+ }))));
979
+ let timer;
980
+ const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
981
+ timer = setTimeout(() => resolve(true), deadline);
982
+ })]);
983
+ clearTimeout(timer);
984
+ if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
985
+ activityId: activityId.href,
986
+ deadline
800
987
  });
801
- if (expanded.length !== 1) return null;
802
- const id = expanded[0]["@id"];
803
- return typeof id === "string" ? id : null;
804
988
  }
805
- function isRequestedPortableId(id, canonicalId) {
806
- if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
807
- try {
808
- return canonicalizePortableUri(id) === canonicalId;
989
+ async function enqueueForwarding(parameters, outboxQueue, inboxes) {
990
+ const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
991
+ const logger = getLogger([
992
+ "fedify",
993
+ "federation",
994
+ "inbox"
995
+ ]);
996
+ const key = await resolveForwardingKey(parameters);
997
+ const keys = [];
998
+ if (key != null) try {
999
+ keys.push({
1000
+ keyId: key.keyId.href,
1001
+ privateKey: await exportJwk(key.privateKey)
1002
+ });
809
1003
  } catch (error) {
810
- if (error instanceof TypeError) return false;
811
- throw error;
1004
+ logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
1005
+ keyId: key.keyId.href,
1006
+ activityId: activityId.href,
1007
+ error
1008
+ });
812
1009
  }
1010
+ startQueue?.();
1011
+ const started = (/* @__PURE__ */ new Date()).toISOString();
1012
+ const traceContext = {};
1013
+ propagation.inject(context.active(), traceContext);
1014
+ await Promise.all(inboxes.map(async (inbox) => {
1015
+ const message = {
1016
+ type: "outbox",
1017
+ id: crypto.randomUUID(),
1018
+ baseUrl,
1019
+ keys,
1020
+ activity,
1021
+ activityId: activityId.href,
1022
+ activityType,
1023
+ inbox: inbox.href,
1024
+ sharedInbox: false,
1025
+ started,
1026
+ attempt: 0,
1027
+ headers: {},
1028
+ traceContext
1029
+ };
1030
+ try {
1031
+ await outboxQueue.enqueue(message);
1032
+ } catch (error) {
1033
+ logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
1034
+ activityId: activityId.href,
1035
+ inbox: inbox.href,
1036
+ error
1037
+ });
1038
+ return;
1039
+ }
1040
+ recordOutboxEnqueue(meterProvider, outboxQueue, message);
1041
+ }));
813
1042
  }
814
- function portableObjectInternalServerError(request) {
815
- return new Response(request.method === "HEAD" ? null : "Internal server error.", {
816
- status: 500,
817
- headers: { "Content-Type": "text/plain; charset=utf-8" }
818
- });
1043
+ //#endregion
1044
+ //#region src/federation/handler.ts
1045
+ const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
1046
+ const INBOX_COMPOUND_PROOF_LIMITS = {
1047
+ maxDepth: 64,
1048
+ maxMaps: 1e4,
1049
+ maxProofs: 32,
1050
+ maxBytes: 10 * 1024 * 1024
1051
+ };
1052
+ function isRemoteContextLoadingFailure$1(error) {
1053
+ return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
819
1054
  }
820
- const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
821
- "inbox",
822
- "outbox",
823
- "following",
824
- "followers",
825
- "liked",
826
- "featured",
827
- "featured_tags"
828
- ]);
829
- function getCollectionMetricKind(name) {
830
- const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
831
- return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
1055
+ function isPermanentRemoteContextError$1(error) {
1056
+ if (!(error instanceof Error) || error.name !== "jsonld.InvalidUrl") return false;
1057
+ const details = error.details;
1058
+ if (details?.code === "invalid remote context") return true;
1059
+ return isRemoteContextLoadingFailure$1(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
832
1060
  }
833
- function collectionAttributes(base, result, response) {
834
- return {
835
- ...base,
836
- result,
837
- ...response == null ? {} : { statusCode: response.status }
838
- };
1061
+ function isInvalidJsonLdError(error) {
1062
+ if (!(error instanceof Error)) return false;
1063
+ const name = error.name;
1064
+ return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
839
1065
  }
840
- function recordCollectionMetrics(meterProvider, base, result, options = {}) {
841
- const attrs = collectionAttributes(base, result, options.response);
842
- recordCollectionRequest(meterProvider, attrs);
843
- if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
844
- if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
845
- if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
1066
+ function isValidationTypeError(error) {
1067
+ return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
1068
+ }
1069
+ function isPermanentActivityParseError(error) {
1070
+ return isInvalidJsonLdError(error) || isValidationTypeError(error);
1071
+ }
1072
+ function hasHttpSignatureHeaders(request) {
1073
+ return request.headers.has("Signature") || request.headers.has("Signature-Input");
1074
+ }
1075
+ function hasObjectIntegrityProof(json) {
1076
+ return typeof json === "object" && json != null && "proof" in json;
846
1077
  }
847
1078
  /**
848
- * Handles a collection request.
849
- * @template TItem The type of items in the collection.
850
- * @template TContext The type of the context, extending {@link RequestContext}.
851
- * @template TContextData The context data to pass to the `TContext`.
852
- * @template TFilter The type of the filter.
1079
+ * Handles an actor request.
1080
+ * @template TContextData The context data to pass to the context.
853
1081
  * @param request The HTTP request.
854
- * @param parameters The parameters for handling the collection.
1082
+ * @param parameters The parameters for handling the actor.
855
1083
  * @returns A promise that resolves to an HTTP response.
856
1084
  */
857
- async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
858
- const spanName = name$2.trim().replace(/\s+/g, "_");
859
- tracerProvider = tracerProvider ?? trace.getTracerProvider();
860
- const tracer = tracerProvider.getTracer(name, version);
861
- const cursor = new URL(request.url).searchParams.get("cursor");
862
- const metricBase = {
1085
+ async function handleActor(request, { identifier, context, actorDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
1086
+ const logger = getLogger([
1087
+ "fedify",
1088
+ "federation",
1089
+ "actor"
1090
+ ]);
1091
+ if (actorDispatcher == null) {
1092
+ logger.debug("Actor dispatcher is not set.", { identifier });
1093
+ return await onNotFound(request);
1094
+ }
1095
+ const actor = await actorDispatcher(context, identifier);
1096
+ if (actor == null) {
1097
+ logger.debug("Actor {identifier} not found.", { identifier });
1098
+ return await onNotFound(request);
1099
+ }
1100
+ if (authorizePredicate != null) {
1101
+ if (!await authorizePredicate(context, identifier)) return await onUnauthorized(request);
1102
+ }
1103
+ if (actor instanceof Tombstone) {
1104
+ const jsonLd = await actor.toJsonLd(context);
1105
+ return new Response(JSON.stringify(jsonLd), {
1106
+ status: 410,
1107
+ headers: {
1108
+ "Content-Type": "application/activity+json",
1109
+ Vary: "Accept"
1110
+ }
1111
+ });
1112
+ }
1113
+ const jsonLd = await actor.toJsonLd(context);
1114
+ return new Response(JSON.stringify(jsonLd), { headers: {
1115
+ "Content-Type": "application/activity+json",
1116
+ Vary: "Accept"
1117
+ } });
1118
+ }
1119
+ /**
1120
+ * Handles an object request.
1121
+ * @template TContextData The context data to pass to the context.
1122
+ * @param request The HTTP request.
1123
+ * @param parameters The parameters for handling the object.
1124
+ * @returns A promise that resolves to an HTTP response.
1125
+ */
1126
+ async function handleObject(request, { values, context, objectDispatcher, authorizePredicate, onNotFound, onUnauthorized }) {
1127
+ if (objectDispatcher == null) return await onNotFound(request);
1128
+ const object = await objectDispatcher(context, values);
1129
+ if (object == null) return await onNotFound(request);
1130
+ if (authorizePredicate != null) {
1131
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
1132
+ }
1133
+ const jsonLd = await object.toJsonLd(context);
1134
+ return new Response(JSON.stringify(jsonLd), { headers: {
1135
+ "Content-Type": "application/activity+json",
1136
+ Vary: "Accept"
1137
+ } });
1138
+ }
1139
+ /**
1140
+ * Handles an FEP-ef61 gateway request for a portable object through an object
1141
+ * dispatcher. The object is served only if its ID canonically matches the
1142
+ * requested portable ID and it satisfies the FEP-ef61 proof policy.
1143
+ * @template TContextData The context data to pass to the context.
1144
+ * @param request The HTTP request.
1145
+ * @param parameters The parameters for handling the portable object.
1146
+ * @returns A promise that resolves to an HTTP response.
1147
+ */
1148
+ async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, onNotFound, onUnauthorized }) {
1149
+ const logger = getLogger([
1150
+ "fedify",
1151
+ "federation",
1152
+ "object"
1153
+ ]);
1154
+ if (objectDispatcher == null) return await onNotFound(request);
1155
+ const object = await objectDispatcher(context, values);
1156
+ if (object == null) return await onNotFound(request);
1157
+ if (!isRequestedPortableObject(object, canonicalId)) {
1158
+ logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
1159
+ objectId: object.id?.href,
1160
+ portableId: canonicalId
1161
+ });
1162
+ return await onNotFound(request);
1163
+ }
1164
+ if (authorizePredicate != null) {
1165
+ if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
1166
+ }
1167
+ let jsonLd;
1168
+ let servedId;
1169
+ let result;
1170
+ try {
1171
+ jsonLd = await object.toJsonLd(context);
1172
+ servedId = await getJsonLdRootId(jsonLd, context.contextLoader);
1173
+ result = await verifyPortableObjectProof(jsonLd, {
1174
+ contextLoader: context.contextLoader,
1175
+ documentLoader: context.documentLoader,
1176
+ tracerProvider: context.tracerProvider,
1177
+ meterProvider: context.meterProvider
1178
+ });
1179
+ } catch (error) {
1180
+ logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
1181
+ portableId: canonicalId,
1182
+ error
1183
+ });
1184
+ return portableObjectInternalServerError(request);
1185
+ }
1186
+ if (servedId == null || !isRequestedPortableId(servedId, canonicalId)) {
1187
+ logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
1188
+ return await onNotFound(request);
1189
+ }
1190
+ if (!result.verified && result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
1191
+ logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID.", {
1192
+ portableId: canonicalId,
1193
+ reason: result.reason.type
1194
+ });
1195
+ return portableObjectInternalServerError(request);
1196
+ }
1197
+ return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
1198
+ "Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
1199
+ Vary: "Accept"
1200
+ } });
1201
+ }
1202
+ function isRequestedPortableObject(object, canonicalId) {
1203
+ return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
1204
+ }
1205
+ async function getJsonLdRootId(jsonLd, contextLoader) {
1206
+ const expanded = await jsonld.expand(jsonLd, {
1207
+ documentLoader: getNormalizationContextLoader(contextLoader),
1208
+ keepFreeFloatingNodes: true
1209
+ });
1210
+ if (expanded.length !== 1) return null;
1211
+ const id = expanded[0]["@id"];
1212
+ return typeof id === "string" ? id : null;
1213
+ }
1214
+ function isRequestedPortableId(id, canonicalId) {
1215
+ if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
1216
+ try {
1217
+ return canonicalizePortableUri(id) === canonicalId;
1218
+ } catch (error) {
1219
+ if (error instanceof TypeError) return false;
1220
+ throw error;
1221
+ }
1222
+ }
1223
+ function portableObjectInternalServerError(request) {
1224
+ return new Response(request.method === "HEAD" ? null : "Internal server error.", {
1225
+ status: 500,
1226
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1227
+ });
1228
+ }
1229
+ const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
1230
+ "inbox",
1231
+ "outbox",
1232
+ "following",
1233
+ "followers",
1234
+ "liked",
1235
+ "featured",
1236
+ "featured_tags"
1237
+ ]);
1238
+ function getCollectionMetricKind(name) {
1239
+ const normalized = name.trim().replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/\s+/g, "_");
1240
+ return BUILT_IN_COLLECTION_METRIC_KINDS.has(normalized) ? normalized : "custom";
1241
+ }
1242
+ function collectionAttributes(base, result, response) {
1243
+ return {
1244
+ ...base,
1245
+ result,
1246
+ ...response == null ? {} : { statusCode: response.status }
1247
+ };
1248
+ }
1249
+ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
1250
+ const attrs = collectionAttributes(base, result, options.response);
1251
+ recordCollectionRequest(meterProvider, attrs);
1252
+ if (options.dispatchDurationMs != null) recordCollectionDispatchDuration(meterProvider, options.dispatchDurationMs, attrs);
1253
+ if (options.itemCount != null) recordCollectionPageItems(meterProvider, options.itemCount, attrs);
1254
+ if (options.totalItems != null) recordCollectionTotalItems(meterProvider, options.totalItems, attrs);
1255
+ }
1256
+ /**
1257
+ * Handles a collection request.
1258
+ * @template TItem The type of items in the collection.
1259
+ * @template TContext The type of the context, extending {@link RequestContext}.
1260
+ * @template TContextData The context data to pass to the `TContext`.
1261
+ * @template TFilter The type of the filter.
1262
+ * @param request The HTTP request.
1263
+ * @param parameters The parameters for handling the collection.
1264
+ * @returns A promise that resolves to an HTTP response.
1265
+ */
1266
+ async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
1267
+ const spanName = name$2.trim().replace(/\s+/g, "_");
1268
+ tracerProvider = tracerProvider ?? trace.getTracerProvider();
1269
+ const tracer = tracerProvider.getTracer(name, version);
1270
+ const cursor = new URL(request.url).searchParams.get("cursor");
1271
+ const metricBase = {
863
1272
  kind: getCollectionMetricKind(name$2),
864
1273
  page: cursor != null,
865
1274
  dispatcher: "built_in"
@@ -1970,7 +2379,21 @@ async function handleInboxInternal(request, parameters, span) {
1970
2379
  inbox: portableInbox.recipient.canonicalInboxId
1971
2380
  });
1972
2381
  else try {
1973
- await portableInbox.forward(json, activity.id, getTypeId(activity).href);
2382
+ await portableInbox.forward(json, activity.id, getTypeId(activity).href, listRequestSignatures(request).map((signature) => ({
2383
+ keyId: signature.keyId,
2384
+ coversDelivery: coversDelivery(signature),
2385
+ verify: async () => {
2386
+ const verification = await verifyRequestDetailed(selectRequestSignature(request, signature), {
2387
+ contextLoader: ctx.contextLoader,
2388
+ documentLoader: ctx.documentLoader,
2389
+ timeWindow: signatureTimeWindow,
2390
+ keyCache,
2391
+ meterProvider,
2392
+ tracerProvider
2393
+ });
2394
+ return verification.verified ? verification.key : null;
2395
+ }
2396
+ })));
1974
2397
  } catch (error) {
1975
2398
  logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
1976
2399
  activityId: activity.id.href,
@@ -2660,308 +3083,6 @@ async function verifyPortableActivityId(activityId, jsonLd, options) {
2660
3083
  return null;
2661
3084
  }
2662
3085
  //#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
- };
2736
- }
2737
- /**
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.
2741
- */
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
- }
2750
- }
2751
- /**
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.
2761
- *
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.
2769
- */
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;
2837
- }
2838
- /**
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.
2843
- */
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;
2862
- 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
- });
2924
- } 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
- });
2930
- }
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
- }));
2963
- }
2964
- //#endregion
2965
3086
  //#region src/federation/outgoing-proof.ts
2966
3087
  function formatKeyIds(candidates) {
2967
3088
  return candidates.map((c) => c.verificationMethod.href).join(", ");
@@ -5457,7 +5578,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5457
5578
  onNotFound,
5458
5579
  portableInbox: {
5459
5580
  recipient,
5460
- forward: async (activity, activityId, activityType) => {
5581
+ forward: async (activity, activityId, activityType, signatures) => {
5461
5582
  if (this.portableInboxForwarding.maxTargets < 1) return;
5462
5583
  if (this.kv.cas == null) {
5463
5584
  if (!this.#portableInboxForwardingWarned) {
@@ -5480,6 +5601,7 @@ var FederationImpl = class extends FederationBuilderImpl {
5480
5601
  allowPrivateAddress: this.allowPrivateAddress,
5481
5602
  getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
5482
5603
  specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
5604
+ signatures,
5483
5605
  options: this.portableInboxForwarding,
5484
5606
  meterProvider: this.meterProvider,
5485
5607
  tracerProvider: this.tracerProvider
@@ -6762,4 +6884,4 @@ function getRequestId(request) {
6762
6884
  return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
6763
6885
  }
6764
6886
  //#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 };
6887
+ 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 };