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