@fedify/fedify 2.4.0-dev.2084 → 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-BGlpUocK.mjs → builder-Bno54o_-.mjs} +34 -3
- 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-B6Aj2kGY.mjs → compound-proof-X3SxXVbc.mjs} +191 -13
- 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-Dn-py9Zy.mjs → deno-dxdvl2XZ.mjs} +1 -1
- package/dist/{docloader-BUq5bYJr.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 +254 -5
- package/dist/federation/portable-gateway-keys.test.mjs +72 -11
- package/dist/federation/portable-inbox.test.mjs +487 -10
- package/dist/federation/portable.test.mjs +182 -4
- 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 +89 -1
- package/dist/{federation-BLGx8JUH.cjs → federation-C4Ef3JKa.cjs} +1487 -1070
- package/dist/{http-B5q1CHtL.cjs → http-C8cZTE3Y.cjs} +1224 -985
- package/dist/{http-4PCCvhsU.js → http-D-rbr_Sy.js} +1663 -1460
- package/dist/{http-B0VRNipE.mjs → http-DsgawTd8.mjs} +82 -7
- package/dist/{key-BJrp524f.mjs → key-DYfmjnmp.mjs} +39 -99
- package/dist/{kv-cache-1KUmAPH7.js → kv-cache-DIxwMeNi.js} +1 -1
- package/dist/{kv-cache-Dhhhrryv.cjs → kv-cache-Dvd-Q-E2.cjs} +1 -1
- package/dist/{kv-cache-C4C3LyYW.mjs → kv-cache-EvOgrFsS.mjs} +1 -1
- package/dist/{ld-BmFz5NLA.mjs → ld-B00wWHzc.mjs} +3 -3
- package/dist/{metrics-Beod6tzO.mjs → metrics-BP3vdBpb.mjs} +1 -1
- package/dist/{middleware-BKeQYPm0.mjs → middleware-Br1_d-Ga.mjs} +606 -389
- package/dist/{middleware-D_9pNYe9.js → middleware-CcXMe400.js} +1602 -1185
- package/dist/{middleware-B7CNsyqt.mjs → middleware-CeBVBa61.mjs} +1 -1
- package/dist/{mod-B2h2KY0q.d.ts → mod-BL5BQHCq.d.ts} +1 -1
- package/dist/{mod-HDSvS-ww.d.cts → mod-C8LieBzF.d.cts} +16 -3
- package/dist/{mod-C9WSpZWm.d.ts → mod-Coacostr.d.ts} +16 -3
- package/dist/{mod-JzkaEXsi.d.cts → mod-DMI4DbOo.d.cts} +1 -1
- package/dist/mod.cjs +5 -5
- package/dist/mod.d.cts +3 -3
- package/dist/mod.d.ts +3 -3
- package/dist/mod.js +5 -5
- package/dist/nodeinfo/handler.test.mjs +1 -1
- package/dist/{owner-CsKVgMzk.mjs → owner-BJPDRDRq.mjs} +5 -4
- package/dist/{owner-BQFZtFV4.cjs → owner-DSzH7wro.cjs} +2 -2
- package/dist/{owner-_pbbgLJK.js → owner-U12fDpQW.js} +2 -2
- package/dist/{portable-key-B4R-CiIV.mjs → portable-key-D0mWbV9G.mjs} +60 -41
- package/dist/portable-key-id-Bva5iT3W.mjs +136 -0
- package/dist/{proof-ozG4HfcQ.mjs → proof-ChjBwnep.mjs} +59 -18
- package/dist/{send-ClPt8nYu.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 +160 -3
- 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.d.cts +1 -1
- package/dist/sig/mod.d.ts +1 -1
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +1 -1
- package/dist/sig/portable-collection.test.mjs +69 -5
- package/dist/sig/portable-key.test.mjs +176 -10
- package/dist/sig/proof.test.mjs +107 -4
- package/dist/{sig-k3C8sF7g.cjs → sig-B689Nj7y.cjs} +19 -3
- package/dist/{sig-DhdomAN0.js → sig-C85KqAcx.js} +19 -3
- package/dist/{temporal-Ba0EAZAI.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 +7 -7
|
@@ -1,27 +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
|
-
import {
|
|
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-DYfmjnmp.mjs";
|
|
8
9
|
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
|
|
9
|
-
import { _ as wrapContextLoaderForJsonLd, a as compactJsonLd, c as getNormalizationContextLoader, d as isClearlyMalformedContextReference, f as isInvalidUrlTypeError, l as hasSignature, m as verifyCompactJsonLd, p as signJsonLd, r as assertSafeJsonLd, s as detachSignature, t as InvalidContextReferenceError, u as hasSignatureLike } from "./ld-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { t as getAuthenticatedDocumentLoader } from "./docloader-
|
|
13
|
-
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";
|
|
14
15
|
import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-DpnpgjyX.mjs";
|
|
15
16
|
import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
|
|
16
|
-
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-
|
|
17
|
-
import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-
|
|
18
|
-
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";
|
|
19
20
|
import { t as CircuitBreaker } from "./circuit-breaker-C5NqoLBJ.mjs";
|
|
20
21
|
import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
|
|
21
|
-
import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-
|
|
22
|
+
import { a as verifyCompoundPortableObjectProofs, i as inspectCompoundPortableObjectApplicability, r as findUnsupportedCompoundProofShape, t as containsCompoundPortableObject } from "./compound-proof-X3SxXVbc.mjs";
|
|
22
23
|
import { t as KvKeyCache } from "./keycache-BUDHB5rY.mjs";
|
|
23
24
|
import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
|
|
24
|
-
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-
|
|
25
|
+
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-Dl9x-Qw4.mjs";
|
|
25
26
|
import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
|
|
26
27
|
import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
|
|
27
28
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
@@ -30,8 +31,9 @@ import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey,
|
|
|
30
31
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
31
32
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
32
33
|
import { uniq } from "es-toolkit";
|
|
33
|
-
import { FetchError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin,
|
|
34
|
+
import { FetchError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, toCompatibleEf61Id } from "@fedify/vocab-runtime";
|
|
34
35
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
36
|
+
import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
35
37
|
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
36
38
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
37
39
|
import { domainToASCII } from "node:url";
|
|
@@ -630,6 +632,415 @@ function parseHashlinkGatewayRequest(url) {
|
|
|
630
632
|
};
|
|
631
633
|
}
|
|
632
634
|
//#endregion
|
|
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
|
+
};
|
|
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
|
+
};
|
|
708
|
+
}
|
|
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
|
+
}
|
|
722
|
+
}
|
|
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));
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
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.
|
|
764
|
+
*/
|
|
765
|
+
async function forwardPortableInboxActivity(parameters) {
|
|
766
|
+
const logger = getLogger([
|
|
767
|
+
"fedify",
|
|
768
|
+
"federation",
|
|
769
|
+
"inbox"
|
|
770
|
+
]);
|
|
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
|
|
790
|
+
});
|
|
791
|
+
await kv.cas(claimKey(sender), void 0, true, { ttl });
|
|
792
|
+
targets = targets.filter((t) => t.gateway !== sender);
|
|
793
|
+
}
|
|
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;
|
|
801
|
+
}
|
|
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;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
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.
|
|
835
|
+
*/
|
|
836
|
+
async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
|
|
837
|
+
if (signatures == null || signatures.length < 1) return null;
|
|
838
|
+
const logger = getLogger([
|
|
839
|
+
"fedify",
|
|
840
|
+
"federation",
|
|
841
|
+
"inbox"
|
|
842
|
+
]);
|
|
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;
|
|
855
|
+
}
|
|
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
|
|
870
|
+
});
|
|
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
|
|
913
|
+
});
|
|
914
|
+
}
|
|
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;
|
|
939
|
+
}
|
|
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
|
|
949
|
+
});
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
return key;
|
|
953
|
+
}
|
|
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
|
|
987
|
+
});
|
|
988
|
+
}
|
|
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
|
+
});
|
|
1003
|
+
} catch (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
|
+
});
|
|
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
|
+
}));
|
|
1042
|
+
}
|
|
1043
|
+
//#endregion
|
|
633
1044
|
//#region src/federation/handler.ts
|
|
634
1045
|
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
635
1046
|
const INBOX_COMPOUND_PROOF_LIMITS = {
|
|
@@ -789,14 +1200,7 @@ async function handlePortableObject(request, { values, context, objectDispatcher
|
|
|
789
1200
|
} });
|
|
790
1201
|
}
|
|
791
1202
|
function isRequestedPortableObject(object, canonicalId) {
|
|
792
|
-
|
|
793
|
-
if (object.id.protocol !== "ap:" && object.id.protocol !== "ap+ef61:") return false;
|
|
794
|
-
try {
|
|
795
|
-
return canonicalizePortableUri(formatIri(object.id)) === canonicalId;
|
|
796
|
-
} catch (error) {
|
|
797
|
-
if (error instanceof TypeError) return false;
|
|
798
|
-
throw error;
|
|
799
|
-
}
|
|
1203
|
+
return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
|
|
800
1204
|
}
|
|
801
1205
|
async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
802
1206
|
const expanded = await jsonld.expand(jsonLd, {
|
|
@@ -808,6 +1212,7 @@ async function getJsonLdRootId(jsonLd, contextLoader) {
|
|
|
808
1212
|
return typeof id === "string" ? id : null;
|
|
809
1213
|
}
|
|
810
1214
|
function isRequestedPortableId(id, canonicalId) {
|
|
1215
|
+
if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
|
|
811
1216
|
try {
|
|
812
1217
|
return canonicalizePortableUri(id) === canonicalId;
|
|
813
1218
|
} catch (error) {
|
|
@@ -858,13 +1263,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
858
1263
|
* @param parameters The parameters for handling the collection.
|
|
859
1264
|
* @returns A promise that resolves to an HTTP response.
|
|
860
1265
|
*/
|
|
861
|
-
async function handleCollection(request, { name: name$
|
|
862
|
-
const spanName = name$
|
|
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, "_");
|
|
863
1268
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
864
1269
|
const tracer = tracerProvider.getTracer(name, version);
|
|
865
1270
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
866
1271
|
const metricBase = {
|
|
867
|
-
kind: getCollectionMetricKind(name$
|
|
1272
|
+
kind: getCollectionMetricKind(name$2),
|
|
868
1273
|
page: cursor != null,
|
|
869
1274
|
dispatcher: "built_in"
|
|
870
1275
|
};
|
|
@@ -905,7 +1310,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
905
1310
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
906
1311
|
return await onNotFound(request);
|
|
907
1312
|
}
|
|
908
|
-
const items = filterCollectionItems(page.items, name$
|
|
1313
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
909
1314
|
itemCount = items.length;
|
|
910
1315
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
911
1316
|
return items;
|
|
@@ -945,7 +1350,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
945
1350
|
} else {
|
|
946
1351
|
const uri = new URL(baseUri);
|
|
947
1352
|
uri.searchParams.set("cursor", cursor);
|
|
948
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
1353
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
|
|
949
1354
|
kind: SpanKind.SERVER,
|
|
950
1355
|
attributes: {
|
|
951
1356
|
"activitypub.collection.id": uri.href,
|
|
@@ -961,7 +1366,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
961
1366
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
962
1367
|
return await onNotFound(request);
|
|
963
1368
|
}
|
|
964
|
-
const items = filterCollectionItems(page.items, name$
|
|
1369
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
965
1370
|
itemCount = items.length;
|
|
966
1371
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
967
1372
|
return {
|
|
@@ -1670,7 +2075,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1670
2075
|
return await respondInvalidActivity(error);
|
|
1671
2076
|
}
|
|
1672
2077
|
activityVerified = true;
|
|
1673
|
-
if (!skipSignatureVerification &&
|
|
2078
|
+
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
1674
2079
|
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
1675
2080
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
1676
2081
|
documentLoader: ctx.documentLoader,
|
|
@@ -1854,7 +2259,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1854
2259
|
"http_signatures.verified": httpSigKey != null,
|
|
1855
2260
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
1856
2261
|
});
|
|
1857
|
-
if (!skipSignatureVerification && !proofVerified && (
|
|
2262
|
+
if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableGatewayKey(httpSigKey))) {
|
|
1858
2263
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
1859
2264
|
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
1860
2265
|
activity: json,
|
|
@@ -1872,6 +2277,32 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1872
2277
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1873
2278
|
});
|
|
1874
2279
|
}
|
|
2280
|
+
if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
|
|
2281
|
+
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
2282
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
2283
|
+
documentLoader: ctx.documentLoader,
|
|
2284
|
+
keyCache,
|
|
2285
|
+
meterProvider,
|
|
2286
|
+
tracerProvider
|
|
2287
|
+
});
|
|
2288
|
+
if (rejection != null) {
|
|
2289
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2290
|
+
logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
|
|
2291
|
+
activity: json,
|
|
2292
|
+
recipient,
|
|
2293
|
+
activityId: activity.id.href,
|
|
2294
|
+
reason: rejection
|
|
2295
|
+
});
|
|
2296
|
+
span.setStatus({
|
|
2297
|
+
code: SpanStatusCode.ERROR,
|
|
2298
|
+
message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
|
|
2299
|
+
});
|
|
2300
|
+
return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
|
|
2301
|
+
status: 401,
|
|
2302
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
1875
2306
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
1876
2307
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
1877
2308
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
@@ -1948,7 +2379,21 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1948
2379
|
inbox: portableInbox.recipient.canonicalInboxId
|
|
1949
2380
|
});
|
|
1950
2381
|
else try {
|
|
1951
|
-
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
|
+
})));
|
|
1952
2397
|
} catch (error) {
|
|
1953
2398
|
logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
|
|
1954
2399
|
activityId: activity.id.href,
|
|
@@ -2109,8 +2554,8 @@ var CustomCollectionHandler = class {
|
|
|
2109
2554
|
* @param CollectionPage The CollectionPage constructor.
|
|
2110
2555
|
* @param filterPredicate Optional filter predicate for items.
|
|
2111
2556
|
*/
|
|
2112
|
-
constructor(name$
|
|
2113
|
-
this.name = name$
|
|
2557
|
+
constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
|
|
2558
|
+
this.name = name$1;
|
|
2114
2559
|
this.values = values;
|
|
2115
2560
|
this.context = context;
|
|
2116
2561
|
this.callbacks = callbacks;
|
|
@@ -2562,380 +3007,127 @@ const getFailedSignatureHeaders = async (policy, kv, kvPrefixes) => ({
|
|
|
2562
3007
|
}
|
|
2563
3008
|
});
|
|
2564
3009
|
async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
|
|
2565
|
-
const parameters = { created: true };
|
|
2566
|
-
if (policy.requestNonce) {
|
|
2567
|
-
const nonce = generateNonce();
|
|
2568
|
-
await setKey(kv, [...noncePrefix, nonce], policy);
|
|
2569
|
-
parameters.nonce = nonce;
|
|
2570
|
-
}
|
|
2571
|
-
const baseComponents = policy.components ?? DEF_COMPONENTS;
|
|
2572
|
-
return formatAcceptSignature([{
|
|
2573
|
-
label: "sig1",
|
|
2574
|
-
components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
|
|
2575
|
-
value: v,
|
|
2576
|
-
params: {}
|
|
2577
|
-
})),
|
|
2578
|
-
parameters
|
|
2579
|
-
}]);
|
|
2580
|
-
}
|
|
2581
|
-
async function setKey(kv, key, policy) {
|
|
2582
|
-
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
2583
|
-
const ttl = Temporal.Duration.from({ seconds });
|
|
2584
|
-
await kv.set(key, true, { ttl });
|
|
2585
|
-
}
|
|
2586
|
-
const DEF_COMPONENTS = [
|
|
2587
|
-
"@method",
|
|
2588
|
-
"@target-uri",
|
|
2589
|
-
"@authority",
|
|
2590
|
-
"content-digest"
|
|
2591
|
-
];
|
|
2592
|
-
const MIN_COMPONENTS = [
|
|
2593
|
-
"@method",
|
|
2594
|
-
"@target-uri",
|
|
2595
|
-
"@authority"
|
|
2596
|
-
];
|
|
2597
|
-
/**
|
|
2598
|
-
* Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
|
|
2599
|
-
* portable actor, i.e., its ID is a compatible identifier and it is owned by
|
|
2600
|
-
* a portable actor, which only the portable actor's signed document can
|
|
2601
|
-
* establish.
|
|
2602
|
-
*/
|
|
2603
|
-
function isPortableGatewayKey(key) {
|
|
2604
|
-
return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableUri(key.ownerId);
|
|
2605
|
-
}
|
|
2606
|
-
//#endregion
|
|
2607
|
-
//#region src/federation/portable-inbox.ts
|
|
2608
|
-
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
2609
|
-
/**
|
|
2610
|
-
* Fills in the defaults of {@link PortableInboxForwardingOptions} and
|
|
2611
|
-
* validates them.
|
|
2612
|
-
* @param options The options.
|
|
2613
|
-
* @returns The resolved options.
|
|
2614
|
-
* @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
|
|
2615
|
-
* is not positive, if `deadline` is negative or longer
|
|
2616
|
-
* than 2,147,483,647 milliseconds (about 24.8 days), or
|
|
2617
|
-
* if either duration has calendar units, i.e., weeks,
|
|
2618
|
-
* months, or years, whose length depends on the date.
|
|
2619
|
-
*/
|
|
2620
|
-
function resolvePortableInboxForwardingOptions(options = {}) {
|
|
2621
|
-
const maxTargets = options.maxTargets ?? 10;
|
|
2622
|
-
if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
|
|
2623
|
-
const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
|
|
2624
|
-
if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
|
|
2625
|
-
const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
|
|
2626
|
-
const deadlineMs = deadline.total("millisecond");
|
|
2627
|
-
if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
2628
|
-
return {
|
|
2629
|
-
maxTargets,
|
|
2630
|
-
ttl,
|
|
2631
|
-
deadline
|
|
2632
|
-
};
|
|
2633
|
-
}
|
|
2634
|
-
/**
|
|
2635
|
-
* Decides whether this server accepts a delivery to a portable inbox on behalf
|
|
2636
|
-
* of the given actor, which the actor dispatcher returned for the identifier
|
|
2637
|
-
* in the requested inbox path.
|
|
2638
|
-
*
|
|
2639
|
-
* The actor document comes from the application, so it is trusted as is and
|
|
2640
|
-
* its proof is not verified here.
|
|
2641
|
-
* @param actor The actor the actor dispatcher returned.
|
|
2642
|
-
* @param options The requested DID, the canonical form of the requested inbox
|
|
2643
|
-
* ID, and the origin of this server.
|
|
2644
|
-
* @returns The resolution.
|
|
2645
|
-
*/
|
|
2646
|
-
function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
|
|
2647
|
-
if (actor == null) return {
|
|
2648
|
-
status: "rejected",
|
|
2649
|
-
reason: "notFound"
|
|
2650
|
-
};
|
|
2651
|
-
const id = actor.id;
|
|
2652
|
-
if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
|
|
2653
|
-
status: "rejected",
|
|
2654
|
-
reason: "notPortable"
|
|
2655
|
-
};
|
|
2656
|
-
if (!isSameAuthority(id, authority)) return {
|
|
2657
|
-
status: "rejected",
|
|
2658
|
-
reason: "authorityMismatch"
|
|
2659
|
-
};
|
|
2660
|
-
const inboxId = actor.inboxId;
|
|
2661
|
-
if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
|
|
2662
|
-
status: "rejected",
|
|
2663
|
-
reason: "inboxMismatch"
|
|
2664
|
-
};
|
|
2665
|
-
const gateways = actor.gateways;
|
|
2666
|
-
if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
|
|
2667
|
-
status: "rejected",
|
|
2668
|
-
reason: "notGateway"
|
|
2669
|
-
};
|
|
2670
|
-
return {
|
|
2671
|
-
status: "accepted",
|
|
2672
|
-
recipient: {
|
|
2673
|
-
actorId: id,
|
|
2674
|
-
inboxId,
|
|
2675
|
-
canonicalInboxId,
|
|
2676
|
-
gateways
|
|
2677
|
-
}
|
|
2678
|
-
};
|
|
2679
|
-
}
|
|
2680
|
-
function isSameAuthority(id, authority) {
|
|
2681
|
-
try {
|
|
2682
|
-
return getFe34Origin(id) === authority;
|
|
2683
|
-
} catch (error) {
|
|
2684
|
-
if (error instanceof TypeError) return false;
|
|
2685
|
-
throw error;
|
|
3010
|
+
const parameters = { created: true };
|
|
3011
|
+
if (policy.requestNonce) {
|
|
3012
|
+
const nonce = generateNonce();
|
|
3013
|
+
await setKey(kv, [...noncePrefix, nonce], policy);
|
|
3014
|
+
parameters.nonce = nonce;
|
|
2686
3015
|
}
|
|
3016
|
+
const baseComponents = policy.components ?? DEF_COMPONENTS;
|
|
3017
|
+
return formatAcceptSignature([{
|
|
3018
|
+
label: "sig1",
|
|
3019
|
+
components: uniq(MIN_COMPONENTS.concat(baseComponents)).filter((c) => c !== "@status").map((v) => ({
|
|
3020
|
+
value: v,
|
|
3021
|
+
params: {}
|
|
3022
|
+
})),
|
|
3023
|
+
parameters
|
|
3024
|
+
}]);
|
|
2687
3025
|
}
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
if (error instanceof TypeError) return null;
|
|
2693
|
-
throw error;
|
|
2694
|
-
}
|
|
3026
|
+
async function setKey(kv, key, policy) {
|
|
3027
|
+
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
3028
|
+
const ttl = Temporal.Duration.from({ seconds });
|
|
3029
|
+
await kv.set(key, true, { ttl });
|
|
2695
3030
|
}
|
|
3031
|
+
const DEF_COMPONENTS = [
|
|
3032
|
+
"@method",
|
|
3033
|
+
"@target-uri",
|
|
3034
|
+
"@authority",
|
|
3035
|
+
"content-digest"
|
|
3036
|
+
];
|
|
3037
|
+
const MIN_COMPONENTS = [
|
|
3038
|
+
"@method",
|
|
3039
|
+
"@target-uri",
|
|
3040
|
+
"@authority"
|
|
3041
|
+
];
|
|
2696
3042
|
/**
|
|
2697
|
-
*
|
|
2698
|
-
* portable actor
|
|
2699
|
-
*
|
|
2700
|
-
*
|
|
2701
|
-
* each target gateway is claimed with an atomic compare-and-swap before the
|
|
2702
|
-
* activity is handed off, and a claim is never released, even if the hand-off
|
|
2703
|
-
* fails, since a failed hand-off does not prove that nothing was delivered.
|
|
2704
|
-
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
2705
|
-
* forwarded.
|
|
2706
|
-
*
|
|
2707
|
-
* Forwarded requests are signed with HTTP Signatures by the RSA key among
|
|
2708
|
-
* {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
|
|
2709
|
-
* servers requiring HTTP Signatures accept them, and are sent unsigned
|
|
2710
|
-
* otherwise. Either way, the receiving gateways authenticate the activity by
|
|
2711
|
-
* its own proof.
|
|
2712
|
-
* @param parameters The parameters.
|
|
2713
|
-
* @returns The target inbox URLs that the activity was handed off to.
|
|
3043
|
+
* Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
|
|
3044
|
+
* portable actor, i.e., its ID is a compatible identifier and it is owned by
|
|
3045
|
+
* a portable actor, which only the portable actor's signed document can
|
|
3046
|
+
* establish.
|
|
2714
3047
|
*/
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
"fedify",
|
|
2718
|
-
"federation",
|
|
2719
|
-
"inbox"
|
|
2720
|
-
]);
|
|
2721
|
-
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
2722
|
-
const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
2723
|
-
if (kv.cas == null || maxTargets < 1) return [];
|
|
2724
|
-
const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
|
|
2725
|
-
const targets = getForwardingTargets(recipient, excludedOrigins);
|
|
2726
|
-
if (targets.length > maxTargets) {
|
|
2727
|
-
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.", {
|
|
2728
|
-
inbox: recipient.canonicalInboxId,
|
|
2729
|
-
activityId: activityId.href,
|
|
2730
|
-
max: maxTargets
|
|
2731
|
-
});
|
|
2732
|
-
targets.length = maxTargets;
|
|
2733
|
-
}
|
|
2734
|
-
const claimed = [];
|
|
2735
|
-
for (const { gateway, inbox } of targets) {
|
|
2736
|
-
const key = [
|
|
2737
|
-
...kvPrefix,
|
|
2738
|
-
recipient.canonicalInboxId,
|
|
2739
|
-
canonicalActivityId,
|
|
2740
|
-
gateway
|
|
2741
|
-
];
|
|
2742
|
-
if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
|
|
2743
|
-
}
|
|
2744
|
-
if (claimed.length < 1) return [];
|
|
2745
|
-
logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
|
|
2746
|
-
activityId: activityId.href,
|
|
2747
|
-
inbox: recipient.canonicalInboxId,
|
|
2748
|
-
targets: claimed.map((t) => t.href)
|
|
2749
|
-
});
|
|
2750
|
-
if (outboxQueue == null) await forwardImmediately(parameters, claimed);
|
|
2751
|
-
else await enqueueForwarding(parameters, outboxQueue, claimed);
|
|
2752
|
-
return claimed;
|
|
3048
|
+
function isPortableGatewayKey(key) {
|
|
3049
|
+
return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
|
|
2753
3050
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
|
|
2764
|
-
seen.add(gateway.origin);
|
|
2765
|
-
let inbox;
|
|
2766
|
-
try {
|
|
2767
|
-
inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
|
|
2768
|
-
} catch (error) {
|
|
2769
|
-
if (!(error instanceof TypeError)) throw error;
|
|
2770
|
-
logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
|
|
2771
|
-
gateway: gateway.origin,
|
|
2772
|
-
inbox: recipient.canonicalInboxId
|
|
2773
|
-
});
|
|
2774
|
-
continue;
|
|
2775
|
-
}
|
|
2776
|
-
targets.push({
|
|
2777
|
-
gateway: gateway.origin,
|
|
2778
|
-
inbox
|
|
2779
|
-
});
|
|
2780
|
-
}
|
|
2781
|
-
return targets;
|
|
3051
|
+
/**
|
|
3052
|
+
* Checks whether an activity needs an Object Integrity Proof by its DID:
|
|
3053
|
+
* either it is performed by an [FEP-ef61] portable actor, or its own ID is
|
|
3054
|
+
* portable. Both `ap:` URIs and compatible identifiers count as portable.
|
|
3055
|
+
*
|
|
3056
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
3057
|
+
*/
|
|
3058
|
+
function isPortableActivity(activity) {
|
|
3059
|
+
return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
|
|
2782
3060
|
}
|
|
2783
3061
|
/**
|
|
2784
|
-
*
|
|
2785
|
-
*
|
|
2786
|
-
*
|
|
2787
|
-
*
|
|
3062
|
+
* Verifies that a portable activity's own ID belongs to the DID that signed
|
|
3063
|
+
* it, on the JSON-LD document received. The ID the proof policy checks has
|
|
3064
|
+
* to be the parsed activity's ID, so that a remote context that changes
|
|
3065
|
+
* between loads cannot make the two name different objects.
|
|
3066
|
+
* @returns `null` if the ID is verified, or the reason why it is not.
|
|
2788
3067
|
*/
|
|
2789
|
-
async function
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
"federation",
|
|
2794
|
-
"inbox"
|
|
2795
|
-
]);
|
|
2796
|
-
let key;
|
|
2797
|
-
try {
|
|
2798
|
-
key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
|
|
2799
|
-
} catch (error) {
|
|
2800
|
-
logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
2801
|
-
activityId: activityId.href,
|
|
2802
|
-
error
|
|
2803
|
-
});
|
|
2804
|
-
return null;
|
|
2805
|
-
}
|
|
2806
|
-
if (key == null) return null;
|
|
3068
|
+
async function verifyPortableActivityId(activityId, jsonLd, options) {
|
|
3069
|
+
const expectedId = getCanonicalPortableId(activityId);
|
|
3070
|
+
if (expectedId == null) return "malformed portable ID";
|
|
3071
|
+
let verification;
|
|
2807
3072
|
try {
|
|
2808
|
-
|
|
2809
|
-
} catch (error) {
|
|
2810
|
-
if (!(error instanceof TypeError)) throw error;
|
|
2811
|
-
logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
2812
|
-
keyId: key.keyId.href,
|
|
2813
|
-
activityId: activityId.href,
|
|
2814
|
-
error
|
|
2815
|
-
});
|
|
2816
|
-
return null;
|
|
2817
|
-
}
|
|
2818
|
-
return key;
|
|
2819
|
-
}
|
|
2820
|
-
async function forwardImmediately(parameters, inboxes) {
|
|
2821
|
-
const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, options, meterProvider, tracerProvider } = parameters;
|
|
2822
|
-
const logger = getLogger([
|
|
2823
|
-
"fedify",
|
|
2824
|
-
"federation",
|
|
2825
|
-
"inbox"
|
|
2826
|
-
]);
|
|
2827
|
-
const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
|
|
2828
|
-
const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
|
|
2829
|
-
activity,
|
|
2830
|
-
activityId: activityId.href,
|
|
2831
|
-
activityType,
|
|
2832
|
-
keys: key == null ? [] : [key],
|
|
2833
|
-
inbox,
|
|
2834
|
-
allowPrivateAddress,
|
|
2835
|
-
specDeterminer,
|
|
2836
|
-
meterProvider,
|
|
2837
|
-
tracerProvider
|
|
2838
|
-
}).catch((error) => {
|
|
2839
|
-
logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
2840
|
-
activityId: activityId.href,
|
|
2841
|
-
inbox: inbox.href,
|
|
2842
|
-
error
|
|
2843
|
-
});
|
|
2844
|
-
}))));
|
|
2845
|
-
let timer;
|
|
2846
|
-
const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
|
|
2847
|
-
timer = setTimeout(() => resolve(true), deadline);
|
|
2848
|
-
})]);
|
|
2849
|
-
clearTimeout(timer);
|
|
2850
|
-
if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
|
|
2851
|
-
activityId: activityId.href,
|
|
2852
|
-
deadline
|
|
2853
|
-
});
|
|
2854
|
-
}
|
|
2855
|
-
async function enqueueForwarding(parameters, outboxQueue, inboxes) {
|
|
2856
|
-
const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
|
|
2857
|
-
const logger = getLogger([
|
|
2858
|
-
"fedify",
|
|
2859
|
-
"federation",
|
|
2860
|
-
"inbox"
|
|
2861
|
-
]);
|
|
2862
|
-
const key = await resolveForwardingKey(parameters);
|
|
2863
|
-
const keys = [];
|
|
2864
|
-
if (key != null) try {
|
|
2865
|
-
keys.push({
|
|
2866
|
-
keyId: key.keyId.href,
|
|
2867
|
-
privateKey: await exportJwk(key.privateKey)
|
|
2868
|
-
});
|
|
3073
|
+
verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
|
|
2869
3074
|
} catch (error) {
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
propagation.inject(context.active(), traceContext);
|
|
2880
|
-
await Promise.all(inboxes.map(async (inbox) => {
|
|
2881
|
-
const message = {
|
|
2882
|
-
type: "outbox",
|
|
2883
|
-
id: crypto.randomUUID(),
|
|
2884
|
-
baseUrl,
|
|
2885
|
-
keys,
|
|
2886
|
-
activity,
|
|
2887
|
-
activityId: activityId.href,
|
|
2888
|
-
activityType,
|
|
2889
|
-
inbox: inbox.href,
|
|
2890
|
-
sharedInbox: false,
|
|
2891
|
-
started,
|
|
2892
|
-
attempt: 0,
|
|
2893
|
-
headers: {},
|
|
2894
|
-
traceContext
|
|
2895
|
-
};
|
|
2896
|
-
try {
|
|
2897
|
-
await outboxQueue.enqueue(message);
|
|
2898
|
-
} catch (error) {
|
|
2899
|
-
logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
2900
|
-
activityId: activityId.href,
|
|
2901
|
-
inbox: inbox.href,
|
|
2902
|
-
error
|
|
2903
|
-
});
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
recordOutboxEnqueue(meterProvider, outboxQueue, message);
|
|
2907
|
-
}));
|
|
3075
|
+
if (error instanceof InvalidPortableObjectIdError) return "malformed portable ID";
|
|
3076
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
3077
|
+
return "malformed document";
|
|
3078
|
+
}
|
|
3079
|
+
const { result, root } = verification;
|
|
3080
|
+
if (!result.verified) return result.reason.type;
|
|
3081
|
+
const rootId = root?.["@id"];
|
|
3082
|
+
if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return "the verified document has another ID";
|
|
3083
|
+
return null;
|
|
2908
3084
|
}
|
|
2909
3085
|
//#endregion
|
|
2910
3086
|
//#region src/federation/outgoing-proof.ts
|
|
2911
|
-
function isPortableUrl(url) {
|
|
2912
|
-
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
2913
|
-
}
|
|
2914
3087
|
function formatKeyIds(candidates) {
|
|
2915
3088
|
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
2916
3089
|
}
|
|
2917
3090
|
/**
|
|
2918
3091
|
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
2919
3092
|
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
2920
|
-
* signed by that DID, so its ID has to be a portable ID
|
|
3093
|
+
* signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
|
|
3094
|
+
* `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
|
|
3095
|
+
* actor is one whose ID is such an ID as well.
|
|
2921
3096
|
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
2922
|
-
* its ID is not a portable ID of the actor's DID,
|
|
2923
|
-
* portable actors do not share a DID
|
|
3097
|
+
* its ID is not a portable ID of the actor's DID, its
|
|
3098
|
+
* portable actors do not share a DID, or the ID of one of
|
|
3099
|
+
* them is a malformed compatible identifier.
|
|
2924
3100
|
* @internal
|
|
2925
3101
|
*/
|
|
2926
3102
|
function assertPortableActorActivity(activity) {
|
|
2927
|
-
const portableActors = activity.actorIds.filter(
|
|
3103
|
+
const portableActors = activity.actorIds.filter((id) => isPortableId(id));
|
|
2928
3104
|
if (portableActors.length < 1) return;
|
|
2929
|
-
const
|
|
2930
|
-
|
|
2931
|
-
|
|
3105
|
+
const dids = portableActors.map((actorId) => {
|
|
3106
|
+
const did = getPortableDid(actorId);
|
|
3107
|
+
if (did == null) throw new TypeError(`The activity ${activity.id?.href} is performed by the actor ${actorId.href}, whose ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
3108
|
+
return did;
|
|
3109
|
+
});
|
|
3110
|
+
const did = dids[0];
|
|
3111
|
+
if (dids.some((d) => d !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
|
|
3112
|
+
if (activity.id == null || !isPortableId(activity.id) || getPortableDid(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier, with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
|
|
3113
|
+
if (!isCompatibleEf61Iri(activity.id)) return;
|
|
3114
|
+
const mismatched = portableActors.find((actorId) => isCompatibleEf61Iri(actorId) && actorId.origin !== activity.id?.origin);
|
|
3115
|
+
if (mismatched == null) return;
|
|
3116
|
+
getLogger([
|
|
3117
|
+
"fedify",
|
|
3118
|
+
"federation",
|
|
3119
|
+
"outbox"
|
|
3120
|
+
]).warn("The activity {activityId} and its actor {actorId} are identified by FEP-ef61 compatible identifiers on different gateways. FEP-ef61 requires publishers to use the first gateway in the actor's gateways when constructing compatible identifiers.", {
|
|
3121
|
+
activityId: activity.id.href,
|
|
3122
|
+
actorId: mismatched.href
|
|
3123
|
+
});
|
|
2932
3124
|
}
|
|
2933
3125
|
/**
|
|
2934
3126
|
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
2935
3127
|
*
|
|
2936
3128
|
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
2937
3129
|
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
2938
|
-
* `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
|
|
3130
|
+
* `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
|
|
2939
3131
|
* whose verification method is a DID URL for its DID, even if it is the only
|
|
2940
3132
|
* key; if no key or more than one key qualifies, this function throws.
|
|
2941
3133
|
*
|
|
@@ -2974,7 +3166,7 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
2974
3166
|
break;
|
|
2975
3167
|
}
|
|
2976
3168
|
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isCompatibleKeyId(c.verificationMethod));
|
|
2977
|
-
if (!hasProof && activity.id != null &&
|
|
3169
|
+
if (!hasProof && activity.id != null && isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
2978
3170
|
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
2979
3171
|
activity,
|
|
2980
3172
|
hasProof,
|
|
@@ -3007,8 +3199,9 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
3007
3199
|
* @throws {TypeError} If no key or more than one key qualifies.
|
|
3008
3200
|
*/
|
|
3009
3201
|
function selectPortableActivityKey(activity, activityId, keys) {
|
|
3010
|
-
const activityDid =
|
|
3011
|
-
|
|
3202
|
+
const activityDid = getPortableDid(activityId);
|
|
3203
|
+
if (activityDid == null) throw new TypeError(`Cannot sign the portable activity ${activityId.href}, as its ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
3204
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && getPortableDid(c.verificationMethod) === activityDid);
|
|
3012
3205
|
if (eligible.length !== 1) throw new TypeError(`Cannot choose an Object Integrity Proof key for the portable activity ${activityId.href}: ` + (eligible.length < 1 ? `none of its ${keys.length} Ed25519 keys (${formatKeyIds(keys)}) ` : `${eligible.length} of its Ed25519 keys (${formatKeyIds(eligible)}) `) + `${eligible.length < 1 ? "is" : "are"} a DID URL verification method for ${activityDid}. FEP-ef61 requires a portable activity's proof to be made by its DID, and the map-local compound-proof profile accepts exactly one direct proof per map. Gateway keys, whose IDs are compatible identifiers, never sign proofs. Supply exactly one Ed25519 key whose key ID is a DID URL for ${activityDid}, or sign the activity with signObject() before sending it.`);
|
|
3013
3206
|
getLogger([
|
|
3014
3207
|
"fedify",
|
|
@@ -3255,6 +3448,27 @@ function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsernam
|
|
|
3255
3448
|
};
|
|
3256
3449
|
}
|
|
3257
3450
|
/**
|
|
3451
|
+
* Gets the compatible identifier that a WebFinger response links to for
|
|
3452
|
+
* an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
|
|
3453
|
+
* URI is linked through its compatible identifier on the given gateway, while
|
|
3454
|
+
* an actor identified by a compatible identifier is linked through its ID as
|
|
3455
|
+
* is, so that software without FEP-ef61 support finds the same ID in the actor
|
|
3456
|
+
* document.
|
|
3457
|
+
* @param actorId The portable actor's ID.
|
|
3458
|
+
* @param gateway The actor's first gateway.
|
|
3459
|
+
* @returns The compatible identifier, or `null` if the actor ID is malformed
|
|
3460
|
+
* or cannot be represented as a compatible identifier.
|
|
3461
|
+
*/
|
|
3462
|
+
function getCompatibleActorId(actorId, gateway) {
|
|
3463
|
+
try {
|
|
3464
|
+
if (isPortableUri(actorId)) return toCompatibleEf61Id(actorId, gateway);
|
|
3465
|
+
return fromCompatibleEf61Id(actorId) == null ? null : actorId;
|
|
3466
|
+
} catch (error) {
|
|
3467
|
+
if (error instanceof TypeError) return null;
|
|
3468
|
+
throw error;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3258
3472
|
* Computes the subject and aliases for an FEP-ef61 portable actor, whose
|
|
3259
3473
|
* WebFinger address takes its domain from the first gateway of the actor
|
|
3260
3474
|
* rather than from the actor ID. The canonical `ap:` ID is not emitted,
|
|
@@ -3418,21 +3632,23 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
3418
3632
|
});
|
|
3419
3633
|
let actorUri = context.getActorUri(identifier);
|
|
3420
3634
|
let portable;
|
|
3421
|
-
if (actor.id != null && (actor.id
|
|
3635
|
+
if (actor.id != null && isPortableId(actor.id)) {
|
|
3422
3636
|
const gateway = actor.gateway;
|
|
3423
|
-
|
|
3424
|
-
if (gateway != null && isGatewayUrl(gateway)) try {
|
|
3425
|
-
compatibleId = toCompatibleEf61Id(actor.id, gateway);
|
|
3426
|
-
} catch (error) {
|
|
3427
|
-
if (!(error instanceof TypeError)) throw error;
|
|
3428
|
-
}
|
|
3429
|
-
if (gateway == null || compatibleId == null) {
|
|
3637
|
+
if (gateway == null || !isGatewayUrl(gateway)) {
|
|
3430
3638
|
logger.error("The portable actor {actorId} (identifier: {identifier}) needs an HTTP(S) origin as the first item of its gateways property to be discovered through WebFinger.", {
|
|
3431
3639
|
actorId: formatIri(actor.id),
|
|
3432
3640
|
identifier
|
|
3433
3641
|
});
|
|
3434
3642
|
return await onNotFound(request);
|
|
3435
3643
|
}
|
|
3644
|
+
const compatibleId = getCompatibleActorId(actor.id, gateway);
|
|
3645
|
+
if (compatibleId == null) {
|
|
3646
|
+
logger.error("The portable actor {actorId} (identifier: {identifier}) cannot be discovered through WebFinger, as its ID is malformed or cannot be represented as an FEP-ef61 compatible identifier.", {
|
|
3647
|
+
actorId: formatIri(actor.id),
|
|
3648
|
+
identifier
|
|
3649
|
+
});
|
|
3650
|
+
return await onNotFound(request);
|
|
3651
|
+
}
|
|
3436
3652
|
actorUri = compatibleId;
|
|
3437
3653
|
portable = {
|
|
3438
3654
|
compatibleId,
|
|
@@ -5362,7 +5578,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5362
5578
|
onNotFound,
|
|
5363
5579
|
portableInbox: {
|
|
5364
5580
|
recipient,
|
|
5365
|
-
forward: async (activity, activityId, activityType) => {
|
|
5581
|
+
forward: async (activity, activityId, activityType, signatures) => {
|
|
5366
5582
|
if (this.portableInboxForwarding.maxTargets < 1) return;
|
|
5367
5583
|
if (this.kv.cas == null) {
|
|
5368
5584
|
if (!this.#portableInboxForwardingWarned) {
|
|
@@ -5385,6 +5601,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5385
5601
|
allowPrivateAddress: this.allowPrivateAddress,
|
|
5386
5602
|
getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
|
|
5387
5603
|
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
5604
|
+
signatures,
|
|
5388
5605
|
options: this.portableInboxForwarding,
|
|
5389
5606
|
meterProvider: this.meterProvider,
|
|
5390
5607
|
tracerProvider: this.tracerProvider
|
|
@@ -6667,4 +6884,4 @@ function getRequestId(request) {
|
|
|
6667
6884
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
6668
6885
|
}
|
|
6669
6886
|
//#endregion
|
|
6670
|
-
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 };
|