@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.
- package/dist/{builder-Ld4Y9ymR.mjs → builder-Bno54o_-.mjs} +2 -2
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/transformers.test.mjs +1 -1
- package/dist/{compound-proof-CTx_lewQ.mjs → compound-proof-X3SxXVbc.mjs} +2 -2
- package/dist/{context-CZjZYXFC.d.cts → context-C43spTGI.d.cts} +5 -2
- package/dist/{context-BSywYspi.d.ts → context-MORIQJIh.d.ts} +5 -2
- package/dist/{deno-8t8k_6Yv.mjs → deno-dxdvl2XZ.mjs} +1 -1
- package/dist/{docloader-DJKcsAiF.mjs → docloader-eBxQDsk9.mjs} +2 -2
- package/dist/federation/builder.test.mjs +1 -1
- package/dist/federation/compound-fanout.test.mjs +3 -3
- package/dist/federation/handler-compound-proof.test.mjs +3 -3
- package/dist/federation/handler.test.mjs +5 -5
- package/dist/federation/hashlink-media.test.mjs +2 -2
- package/dist/federation/idempotency.test.mjs +2 -2
- package/dist/federation/metrics.test.mjs +1 -1
- package/dist/federation/middleware.test.mjs +7 -7
- package/dist/federation/mod.cjs +1 -1
- package/dist/federation/mod.d.cts +2 -2
- package/dist/federation/mod.d.ts +2 -2
- package/dist/federation/mod.js +1 -1
- package/dist/federation/outgoing-proof.test.mjs +5 -5
- package/dist/federation/portable-gateway-keys.test.mjs +3 -3
- package/dist/federation/portable-inbox.test.mjs +375 -10
- package/dist/federation/portable.test.mjs +3 -3
- package/dist/federation/send.test.mjs +3 -3
- package/dist/federation/tasks/enqueue.test.mjs +1 -1
- package/dist/federation/tasks/tasks.test.mjs +2 -2
- package/dist/federation/temporal.test.mjs +1 -1
- package/dist/federation/webfinger.test.mjs +1 -1
- package/dist/{federation-aSHO04ob.cjs → federation-C4Ef3JKa.cjs} +1211 -1089
- package/dist/{http-DSaSAN7X.cjs → http-C8cZTE3Y.cjs} +89 -2
- package/dist/{http-DyT7DP1Y.js → http-D-rbr_Sy.js} +78 -3
- package/dist/{http-B2ftY0Ha.mjs → http-DsgawTd8.mjs} +82 -7
- package/dist/{key-Dwm4VswT.mjs → key-DYfmjnmp.mjs} +2 -2
- package/dist/{kv-cache-DmroAz3M.js → kv-cache-DIxwMeNi.js} +1 -1
- package/dist/{kv-cache-BSJAlP4C.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
- package/dist/{kv-cache-D0yYukqk.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
- package/dist/{ld-BYZOoDzZ.mjs → ld-B00wWHzc.mjs} +3 -3
- package/dist/{metrics-B7YwKTFr.mjs → metrics-BP3vdBpb.mjs} +1 -1
- package/dist/{middleware-E_0IH72m.mjs → middleware-Br1_d-Ga.mjs} +627 -505
- package/dist/{middleware-Blq0882d.js → middleware-CcXMe400.js} +1307 -1185
- package/dist/{middleware-BNeqKSsL.mjs → middleware-CeBVBa61.mjs} +1 -1
- package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
- package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +2 -2
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-Cgx2Dc7I.mjs → owner-BJPDRDRq.mjs} +3 -3
- package/dist/{owner-szIZl4qI.cjs → owner-DSzH7wro.cjs} +1 -1
- package/dist/{owner-DAGSC5gX.js → owner-U12fDpQW.js} +1 -1
- package/dist/{portable-key-Cn1WJSN1.mjs → portable-key-D0mWbV9G.mjs} +1 -1
- package/dist/{proof-DVXC6nYz.mjs → proof-ChjBwnep.mjs} +4 -4
- package/dist/{send-D-hOvYKy.mjs → send-yFX0E-ye.mjs} +3 -3
- package/dist/sig/compound-proof-discovery.test.mjs +1 -1
- package/dist/sig/compound-proof-producer.test.mjs +1 -1
- package/dist/sig/compound-proof-verification.test.mjs +2 -2
- package/dist/sig/compound-proof.test.mjs +2 -2
- package/dist/sig/http.test.mjs +72 -2
- package/dist/sig/key.test.mjs +1 -1
- package/dist/sig/ld.test.mjs +2 -2
- package/dist/sig/mod.cjs +3 -3
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +2 -2
- package/dist/sig/portable-key.test.mjs +3 -3
- package/dist/sig/proof.test.mjs +3 -3
- package/dist/{sig-CXOKN4Br.cjs → sig-B689Nj7y.cjs} +2 -2
- package/dist/{sig-B5Ell7pO.js → sig-C85KqAcx.js} +2 -2
- package/dist/{temporal-CqpKGEPk.mjs → temporal-Dl9x-Qw4.mjs} +1 -1
- package/dist/testing/kv.test.mjs +1 -1
- package/dist/testing/mod.d.mts +5 -2
- package/dist/utils/docloader.test.mjs +2 -2
- package/dist/utils/kv-cache.test.mjs +1 -1
- package/dist/utils/mod.cjs +1 -1
- package/dist/utils/mod.js +1 -1
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
11
|
-
import { i as signObject, l as verifyPortableObjectProofWithRoot, o as verifyObject, r as hasProofLike, s as verifyPortableObjectProof, t as InvalidPortableObjectIdError } from "./proof-
|
|
12
|
-
import {
|
|
13
|
-
import { t as getAuthenticatedDocumentLoader } from "./docloader-
|
|
14
|
-
import { n as kvCache } from "./kv-cache-
|
|
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-
|
|
18
|
-
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-
|
|
19
|
-
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-
|
|
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-
|
|
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-
|
|
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/
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
661
|
-
|
|
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
|
-
|
|
664
|
-
|
|
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
|
-
|
|
667
|
-
|
|
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
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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
|
|
765
|
+
async function forwardPortableInboxActivity(parameters) {
|
|
677
766
|
const logger = getLogger([
|
|
678
767
|
"fedify",
|
|
679
768
|
"federation",
|
|
680
|
-
"
|
|
769
|
+
"inbox"
|
|
681
770
|
]);
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
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
|
|
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
|
-
"
|
|
841
|
+
"inbox"
|
|
744
842
|
]);
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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 (
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
|
950
|
+
return null;
|
|
787
951
|
}
|
|
788
|
-
return
|
|
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
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
811
|
-
|
|
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
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
821
|
-
"
|
|
822
|
-
|
|
823
|
-
"
|
|
824
|
-
"
|
|
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
|
|
834
|
-
return
|
|
835
|
-
|
|
836
|
-
|
|
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
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
|
849
|
-
* @template
|
|
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
|
|
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
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
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 {
|
|
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 };
|