@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
|
@@ -2,10 +2,10 @@ import { Temporal } from "temporal-polyfill";
|
|
|
2
2
|
import { URLPattern } from "urlpattern-polyfill";
|
|
3
3
|
import { t as __exportAll } from "./rolldown-runtime-BC-Eo5Mj.js";
|
|
4
4
|
import { r as getDefaultActivityTransformers } from "./transformers-DAtXl_Kc.js";
|
|
5
|
-
import {
|
|
5
|
+
import { $ as recordCollectionPageItems, C as hasSignature, D as signJsonLd, E as isInvalidUrlTypeError, H as hasPortableActor, J as getRemoteHost, K as getDurationMs, L as importJwk, M as exportJwk, O as verifyCompactJsonLd, Q as recordCollectionDispatchDuration, R as validateCryptoKey, S as getNormalizationContextLoader, T as isClearlyMalformedContextReference, U as isCompatibleKeyId, V as getGatewayKeyBase, W as isPortableUri, X as isAbortError, Y as instrumentDocumentLoader, Z as recordCircuitBreakerStateChange, _ as assertSafeJsonLd, a as verifyRequestDetailed, at as recordOutboxActivity, c as hasProofLike, ct as registerQueueDepthGauge, d as verifyObject, et as recordCollectionRequest, f as verifyPortableObjectProof, g as InvalidContextReferenceError, ht as version, i as verifyRequest, it as recordInboxActivity, j as wrapContextLoaderForJsonLd, l as signObject, lt as normalizeOutgoingActivityJsonLd, mt as name, n as parseRfc9421SignatureInput, ot as recordOutboxEnqueue, p as verifyPortableObjectProofPolicy, q as getFederationMetrics, rt as recordFanoutRecipients, st as recordWebFingerHandle, t as doubleKnock, tt as recordCollectionTotalItems, u as verifyMapLocalProof, ut as formatAcceptSignature, w as hasSignatureLike, x as detachSignature, y as compactJsonLd } from "./http-vfTpzGEq.js";
|
|
6
6
|
import { a as MAX_BODY_SIZE, i as BodyTooLargeError, n as getNodeInfo, o as readBoundedText, t as nodeInfoToJson } from "./types-C_cNlNVK.js";
|
|
7
|
-
import {
|
|
8
|
-
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-
|
|
7
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DfGz0cXm.js";
|
|
8
|
+
import { n as getAuthenticatedDocumentLoader, t as kvCache } from "./kv-cache-DCJuwdnB.js";
|
|
9
9
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
10
10
|
import { Router, RouterError, assertPath } from "@fedify/uri-template";
|
|
11
11
|
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
@@ -281,6 +281,10 @@ var FederationBuilderImpl = class {
|
|
|
281
281
|
callbacks.aliasMapper = mapper;
|
|
282
282
|
return setters;
|
|
283
283
|
},
|
|
284
|
+
mapPortableActorId(mapper) {
|
|
285
|
+
callbacks.portableActorIdMapper = mapper;
|
|
286
|
+
return setters;
|
|
287
|
+
},
|
|
284
288
|
mapActorAlias: (path, identifier) => {
|
|
285
289
|
if (identifier === "") throw new RouterError("Identifier cannot be empty.");
|
|
286
290
|
if (this.router.has(`actorAlias:${identifier}`)) throw new RouterError(`Actor alias for "${identifier}" already set.`);
|
|
@@ -3238,7 +3242,7 @@ async function handleInbox(request, options) {
|
|
|
3238
3242
|
* @returns A promise that resolves to an HTTP response.
|
|
3239
3243
|
*/
|
|
3240
3244
|
async function handleInboxInternal(request, parameters, span) {
|
|
3241
|
-
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
|
|
3245
|
+
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
|
|
3242
3246
|
const logger = getLogger([
|
|
3243
3247
|
"fedify",
|
|
3244
3248
|
"federation",
|
|
@@ -3251,7 +3255,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3251
3255
|
message: "Actor dispatcher is not set."
|
|
3252
3256
|
});
|
|
3253
3257
|
return await onNotFound(request);
|
|
3254
|
-
} else if (recipient != null) {
|
|
3258
|
+
} else if (recipient != null && portableInbox == null) {
|
|
3255
3259
|
const actor = await actorDispatcher(ctx, recipient);
|
|
3256
3260
|
if (actor == null || actor instanceof Tombstone) {
|
|
3257
3261
|
logger.error("Actor {recipient} not found.", { recipient });
|
|
@@ -3424,6 +3428,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3424
3428
|
}
|
|
3425
3429
|
let activity = null;
|
|
3426
3430
|
let activityVerified = false;
|
|
3431
|
+
let proofVerified = false;
|
|
3427
3432
|
if (ldSigVerified) {
|
|
3428
3433
|
logger.debug("Linked Data Signatures are verified.", {
|
|
3429
3434
|
recipient,
|
|
@@ -3440,6 +3445,21 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3440
3445
|
return await respondInvalidActivity(error);
|
|
3441
3446
|
}
|
|
3442
3447
|
activityVerified = true;
|
|
3448
|
+
if (!skipSignatureVerification && hasPortableActor(activity)) try {
|
|
3449
|
+
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
3450
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
3451
|
+
documentLoader: ctx.documentLoader,
|
|
3452
|
+
keyCache,
|
|
3453
|
+
meterProvider,
|
|
3454
|
+
tracerProvider
|
|
3455
|
+
}) != null;
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
3458
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the portable actor's activity:\n{error}", {
|
|
3459
|
+
recipient,
|
|
3460
|
+
error
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
3443
3463
|
} else {
|
|
3444
3464
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
3445
3465
|
recipient,
|
|
@@ -3496,6 +3516,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3496
3516
|
activity: json
|
|
3497
3517
|
});
|
|
3498
3518
|
activityVerified = true;
|
|
3519
|
+
proofVerified = true;
|
|
3499
3520
|
}
|
|
3500
3521
|
}
|
|
3501
3522
|
let httpSigKey = null;
|
|
@@ -3608,6 +3629,24 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3608
3629
|
"http_signatures.verified": httpSigKey != null,
|
|
3609
3630
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
3610
3631
|
});
|
|
3632
|
+
if (!skipSignatureVerification && !proofVerified && (hasPortableActor(activity) || isPortableGatewayKey(httpSigKey))) {
|
|
3633
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3634
|
+
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
3635
|
+
activity: json,
|
|
3636
|
+
recipient,
|
|
3637
|
+
activityId: activity.id?.href,
|
|
3638
|
+
actorId: activity.actorId?.href,
|
|
3639
|
+
keyId: httpSigKey?.id?.href
|
|
3640
|
+
});
|
|
3641
|
+
span.setStatus({
|
|
3642
|
+
code: SpanStatusCode.ERROR,
|
|
3643
|
+
message: `The activity of the portable actor (${activity.actorId?.href}) is not authenticated by a valid Object Integrity Proof.`
|
|
3644
|
+
});
|
|
3645
|
+
return new Response("Activities of portable actors must have valid Object Integrity Proofs.", {
|
|
3646
|
+
status: 401,
|
|
3647
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3611
3650
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
3612
3651
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
3613
3652
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
@@ -3678,6 +3717,19 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3678
3717
|
tracerProvider,
|
|
3679
3718
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
3680
3719
|
});
|
|
3720
|
+
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.", {
|
|
3721
|
+
activityId: activity.id.href,
|
|
3722
|
+
inbox: portableInbox.recipient.canonicalInboxId
|
|
3723
|
+
});
|
|
3724
|
+
else try {
|
|
3725
|
+
await portableInbox.forward(json, activity.id, getTypeId(activity).href);
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
|
|
3728
|
+
activityId: activity.id.href,
|
|
3729
|
+
inbox: portableInbox.recipient.canonicalInboxId,
|
|
3730
|
+
error
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3681
3733
|
if (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
|
|
3682
3734
|
status: 202,
|
|
3683
3735
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -3703,6 +3755,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
3703
3755
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3704
3756
|
});
|
|
3705
3757
|
}
|
|
3758
|
+
const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
|
|
3759
|
+
"success",
|
|
3760
|
+
"enqueued",
|
|
3761
|
+
"alreadyProcessed",
|
|
3762
|
+
"unsupportedActivity"
|
|
3763
|
+
]);
|
|
3706
3764
|
/**
|
|
3707
3765
|
* Handles a custom collection request.
|
|
3708
3766
|
* @template TItem The type of items in the collection.
|
|
@@ -4310,6 +4368,15 @@ const MIN_COMPONENTS = [
|
|
|
4310
4368
|
"@target-uri",
|
|
4311
4369
|
"@authority"
|
|
4312
4370
|
];
|
|
4371
|
+
/**
|
|
4372
|
+
* Checks whether an HTTP Signature key is a gateway key of an FEP-ef61
|
|
4373
|
+
* portable actor, i.e., its ID is a compatible identifier and it is owned by
|
|
4374
|
+
* a portable actor, which only the portable actor's signed document can
|
|
4375
|
+
* establish.
|
|
4376
|
+
*/
|
|
4377
|
+
function isPortableGatewayKey(key) {
|
|
4378
|
+
return key?.id != null && isCompatibleKeyId(key.id) && key.ownerId != null && isPortableUri(key.ownerId);
|
|
4379
|
+
}
|
|
4313
4380
|
//#endregion
|
|
4314
4381
|
//#region src/nodeinfo/handler.ts
|
|
4315
4382
|
/**
|
|
@@ -4485,7 +4552,11 @@ async function sendActivityInternal({ activity, activityId, activityType, keys,
|
|
|
4485
4552
|
rsaKey = key;
|
|
4486
4553
|
break;
|
|
4487
4554
|
}
|
|
4488
|
-
if (rsaKey == null) logger.
|
|
4555
|
+
if (rsaKey == null && keys.length < 1) logger.debug("Sending the activity {activityId} to {inbox} without a signature.", {
|
|
4556
|
+
activityId,
|
|
4557
|
+
inbox: inbox.href
|
|
4558
|
+
});
|
|
4559
|
+
else if (rsaKey == null) logger.warn("No supported key found to sign the request to {inbox}. The request will be sent without a signature. In order to sign the request, at least one RSASSA-PKCS1-v1_5 key must be provided.", {
|
|
4489
4560
|
inbox: inbox.href,
|
|
4490
4561
|
keys: keys.map((pair) => ({
|
|
4491
4562
|
keyId: pair.keyId.href,
|
|
@@ -4827,6 +4898,251 @@ function serializeDataPointType(dataPointType) {
|
|
|
4827
4898
|
}
|
|
4828
4899
|
}
|
|
4829
4900
|
//#endregion
|
|
4901
|
+
//#region src/federation/portable-inbox.ts
|
|
4902
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
4903
|
+
/**
|
|
4904
|
+
* Fills in the defaults of {@link PortableInboxForwardingOptions} and
|
|
4905
|
+
* validates them.
|
|
4906
|
+
* @param options The options.
|
|
4907
|
+
* @returns The resolved options.
|
|
4908
|
+
* @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
|
|
4909
|
+
* is not positive, if `deadline` is negative or longer
|
|
4910
|
+
* than 2,147,483,647 milliseconds (about 24.8 days), or
|
|
4911
|
+
* if either duration has calendar units, i.e., weeks,
|
|
4912
|
+
* months, or years, whose length depends on the date.
|
|
4913
|
+
*/
|
|
4914
|
+
function resolvePortableInboxForwardingOptions(options = {}) {
|
|
4915
|
+
const maxTargets = options.maxTargets ?? 10;
|
|
4916
|
+
if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
|
|
4917
|
+
const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
|
|
4918
|
+
if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
|
|
4919
|
+
const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
|
|
4920
|
+
const deadlineMs = deadline.total("millisecond");
|
|
4921
|
+
if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
4922
|
+
return {
|
|
4923
|
+
maxTargets,
|
|
4924
|
+
ttl,
|
|
4925
|
+
deadline
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4928
|
+
/**
|
|
4929
|
+
* Decides whether this server accepts a delivery to a portable inbox on behalf
|
|
4930
|
+
* of the given actor, which the actor dispatcher returned for the identifier
|
|
4931
|
+
* in the requested inbox path.
|
|
4932
|
+
*
|
|
4933
|
+
* The actor document comes from the application, so it is trusted as is and
|
|
4934
|
+
* its proof is not verified here.
|
|
4935
|
+
* @param actor The actor the actor dispatcher returned.
|
|
4936
|
+
* @param options The requested DID, the canonical form of the requested inbox
|
|
4937
|
+
* ID, and the origin of this server.
|
|
4938
|
+
* @returns The resolution.
|
|
4939
|
+
*/
|
|
4940
|
+
function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
|
|
4941
|
+
if (actor == null) return {
|
|
4942
|
+
status: "rejected",
|
|
4943
|
+
reason: "notFound"
|
|
4944
|
+
};
|
|
4945
|
+
const id = actor.id;
|
|
4946
|
+
if (id == null || id.protocol !== "ap:" && id.protocol !== "ap+ef61:") return {
|
|
4947
|
+
status: "rejected",
|
|
4948
|
+
reason: "notPortable"
|
|
4949
|
+
};
|
|
4950
|
+
if (!isSameAuthority(id, authority)) return {
|
|
4951
|
+
status: "rejected",
|
|
4952
|
+
reason: "authorityMismatch"
|
|
4953
|
+
};
|
|
4954
|
+
const inboxId = actor.inboxId;
|
|
4955
|
+
if (inboxId == null || canonicalize(inboxId) !== canonicalInboxId) return {
|
|
4956
|
+
status: "rejected",
|
|
4957
|
+
reason: "inboxMismatch"
|
|
4958
|
+
};
|
|
4959
|
+
const gateways = actor.gateways;
|
|
4960
|
+
if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
|
|
4961
|
+
status: "rejected",
|
|
4962
|
+
reason: "notGateway"
|
|
4963
|
+
};
|
|
4964
|
+
return {
|
|
4965
|
+
status: "accepted",
|
|
4966
|
+
recipient: {
|
|
4967
|
+
inboxId,
|
|
4968
|
+
canonicalInboxId,
|
|
4969
|
+
gateways
|
|
4970
|
+
}
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
function isSameAuthority(id, authority) {
|
|
4974
|
+
try {
|
|
4975
|
+
return getFe34Origin(id) === authority;
|
|
4976
|
+
} catch (error) {
|
|
4977
|
+
if (error instanceof TypeError) return false;
|
|
4978
|
+
throw error;
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
function canonicalize(iri) {
|
|
4982
|
+
try {
|
|
4983
|
+
return canonicalizePortableUri(typeof iri === "string" ? iri : formatIri(iri));
|
|
4984
|
+
} catch (error) {
|
|
4985
|
+
if (error instanceof TypeError) return null;
|
|
4986
|
+
throw error;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
/**
|
|
4990
|
+
* Forwards an activity received in a portable inbox to the inboxes of the same
|
|
4991
|
+
* portable actor on its other gateways, as FEP-ef61 recommends.
|
|
4992
|
+
*
|
|
4993
|
+
* FEP-ef61 forbids forwarding an activity from an inbox more than once, so
|
|
4994
|
+
* each target gateway is claimed with an atomic compare-and-swap before the
|
|
4995
|
+
* activity is handed off, and a claim is never released, even if the hand-off
|
|
4996
|
+
* fails, since a failed hand-off does not prove that nothing was delivered.
|
|
4997
|
+
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
4998
|
+
* forwarded.
|
|
4999
|
+
*
|
|
5000
|
+
* Forwarded requests are not signed with HTTP Signatures; the receiving
|
|
5001
|
+
* gateways authenticate the activity by its own proof.
|
|
5002
|
+
* @param parameters The parameters.
|
|
5003
|
+
* @returns The target inbox URLs that the activity was handed off to.
|
|
5004
|
+
*/
|
|
5005
|
+
async function forwardPortableInboxActivity(parameters) {
|
|
5006
|
+
const logger = getLogger([
|
|
5007
|
+
"fedify",
|
|
5008
|
+
"federation",
|
|
5009
|
+
"inbox"
|
|
5010
|
+
]);
|
|
5011
|
+
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
5012
|
+
const { maxTargets, ttl } = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
5013
|
+
if (kv.cas == null || maxTargets < 1) return [];
|
|
5014
|
+
const canonicalActivityId = canonicalize(activityId) ?? activityId.href;
|
|
5015
|
+
const targets = getForwardingTargets(recipient, excludedOrigins);
|
|
5016
|
+
if (targets.length > maxTargets) {
|
|
5017
|
+
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.", {
|
|
5018
|
+
inbox: recipient.canonicalInboxId,
|
|
5019
|
+
activityId: activityId.href,
|
|
5020
|
+
max: maxTargets
|
|
5021
|
+
});
|
|
5022
|
+
targets.length = maxTargets;
|
|
5023
|
+
}
|
|
5024
|
+
const claimed = [];
|
|
5025
|
+
for (const { gateway, inbox } of targets) {
|
|
5026
|
+
const key = [
|
|
5027
|
+
...kvPrefix,
|
|
5028
|
+
recipient.canonicalInboxId,
|
|
5029
|
+
canonicalActivityId,
|
|
5030
|
+
gateway
|
|
5031
|
+
];
|
|
5032
|
+
if (await kv.cas(key, void 0, true, { ttl })) claimed.push(inbox);
|
|
5033
|
+
}
|
|
5034
|
+
if (claimed.length < 1) return [];
|
|
5035
|
+
logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
|
|
5036
|
+
activityId: activityId.href,
|
|
5037
|
+
inbox: recipient.canonicalInboxId,
|
|
5038
|
+
targets: claimed.map((t) => t.href)
|
|
5039
|
+
});
|
|
5040
|
+
if (outboxQueue == null) await forwardImmediately(parameters, claimed);
|
|
5041
|
+
else await enqueueForwarding(parameters, outboxQueue, claimed);
|
|
5042
|
+
return claimed;
|
|
5043
|
+
}
|
|
5044
|
+
function getForwardingTargets(recipient, excludedOrigins) {
|
|
5045
|
+
const logger = getLogger([
|
|
5046
|
+
"fedify",
|
|
5047
|
+
"federation",
|
|
5048
|
+
"inbox"
|
|
5049
|
+
]);
|
|
5050
|
+
const seen = new Set(excludedOrigins);
|
|
5051
|
+
const targets = [];
|
|
5052
|
+
for (const gateway of recipient.gateways) {
|
|
5053
|
+
if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
|
|
5054
|
+
seen.add(gateway.origin);
|
|
5055
|
+
let inbox;
|
|
5056
|
+
try {
|
|
5057
|
+
inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
|
|
5058
|
+
} catch (error) {
|
|
5059
|
+
if (!(error instanceof TypeError)) throw error;
|
|
5060
|
+
logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
|
|
5061
|
+
gateway: gateway.origin,
|
|
5062
|
+
inbox: recipient.canonicalInboxId
|
|
5063
|
+
});
|
|
5064
|
+
continue;
|
|
5065
|
+
}
|
|
5066
|
+
targets.push({
|
|
5067
|
+
gateway: gateway.origin,
|
|
5068
|
+
inbox
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
return targets;
|
|
5072
|
+
}
|
|
5073
|
+
async function forwardImmediately({ activity, activityId, activityType, allowPrivateAddress, options, meterProvider, tracerProvider }, inboxes) {
|
|
5074
|
+
const logger = getLogger([
|
|
5075
|
+
"fedify",
|
|
5076
|
+
"federation",
|
|
5077
|
+
"inbox"
|
|
5078
|
+
]);
|
|
5079
|
+
const deadline = (options ?? resolvePortableInboxForwardingOptions()).deadline.total("millisecond");
|
|
5080
|
+
const sends = inboxes.map((inbox) => sendActivity({
|
|
5081
|
+
activity,
|
|
5082
|
+
activityId: activityId.href,
|
|
5083
|
+
activityType,
|
|
5084
|
+
keys: [],
|
|
5085
|
+
inbox,
|
|
5086
|
+
allowPrivateAddress,
|
|
5087
|
+
meterProvider,
|
|
5088
|
+
tracerProvider
|
|
5089
|
+
}).catch((error) => {
|
|
5090
|
+
logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
5091
|
+
activityId: activityId.href,
|
|
5092
|
+
inbox: inbox.href,
|
|
5093
|
+
error
|
|
5094
|
+
});
|
|
5095
|
+
}));
|
|
5096
|
+
let timer;
|
|
5097
|
+
const timedOut = await Promise.race([Promise.all(sends).then(() => false), new Promise((resolve) => {
|
|
5098
|
+
timer = setTimeout(() => resolve(true), deadline);
|
|
5099
|
+
})]);
|
|
5100
|
+
clearTimeout(timer);
|
|
5101
|
+
if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
|
|
5102
|
+
activityId: activityId.href,
|
|
5103
|
+
deadline
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
async function enqueueForwarding({ activity, activityId, activityType, baseUrl, startQueue, meterProvider }, outboxQueue, inboxes) {
|
|
5107
|
+
const logger = getLogger([
|
|
5108
|
+
"fedify",
|
|
5109
|
+
"federation",
|
|
5110
|
+
"inbox"
|
|
5111
|
+
]);
|
|
5112
|
+
startQueue?.();
|
|
5113
|
+
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
5114
|
+
const traceContext = {};
|
|
5115
|
+
propagation.inject(context.active(), traceContext);
|
|
5116
|
+
await Promise.all(inboxes.map(async (inbox) => {
|
|
5117
|
+
const message = {
|
|
5118
|
+
type: "outbox",
|
|
5119
|
+
id: crypto.randomUUID(),
|
|
5120
|
+
baseUrl,
|
|
5121
|
+
keys: [],
|
|
5122
|
+
activity,
|
|
5123
|
+
activityId: activityId.href,
|
|
5124
|
+
activityType,
|
|
5125
|
+
inbox: inbox.href,
|
|
5126
|
+
sharedInbox: false,
|
|
5127
|
+
started,
|
|
5128
|
+
attempt: 0,
|
|
5129
|
+
headers: {},
|
|
5130
|
+
traceContext
|
|
5131
|
+
};
|
|
5132
|
+
try {
|
|
5133
|
+
await outboxQueue.enqueue(message);
|
|
5134
|
+
} catch (error) {
|
|
5135
|
+
logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
5136
|
+
activityId: activityId.href,
|
|
5137
|
+
inbox: inbox.href,
|
|
5138
|
+
error
|
|
5139
|
+
});
|
|
5140
|
+
return;
|
|
5141
|
+
}
|
|
5142
|
+
recordOutboxEnqueue(meterProvider, outboxQueue, message);
|
|
5143
|
+
}));
|
|
5144
|
+
}
|
|
5145
|
+
//#endregion
|
|
4830
5146
|
//#region src/federation/outgoing-proof.ts
|
|
4831
5147
|
function isPortableUrl(url) {
|
|
4832
5148
|
return url.protocol === "ap:" || url.protocol === "ap+ef61:";
|
|
@@ -4835,37 +5151,48 @@ function formatKeyIds(candidates) {
|
|
|
4835
5151
|
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
4836
5152
|
}
|
|
4837
5153
|
/**
|
|
5154
|
+
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
5155
|
+
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
5156
|
+
* signed by that DID, so its ID has to be a portable ID with the same DID.
|
|
5157
|
+
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
5158
|
+
* its ID is not a portable ID of the actor's DID, or its
|
|
5159
|
+
* portable actors do not share a DID.
|
|
5160
|
+
* @internal
|
|
5161
|
+
*/
|
|
5162
|
+
function assertPortableActorActivity(activity) {
|
|
5163
|
+
const portableActors = activity.actorIds.filter(isPortableUrl);
|
|
5164
|
+
if (portableActors.length < 1) return;
|
|
5165
|
+
const did = getFe34Origin(portableActors[0]);
|
|
5166
|
+
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.`);
|
|
5167
|
+
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.`);
|
|
5168
|
+
}
|
|
5169
|
+
/**
|
|
4838
5170
|
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
4839
5171
|
*
|
|
5172
|
+
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
5173
|
+
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
5174
|
+
* `ap+ef61:` ID, that carries no proof is signed only by the one Ed25519 key
|
|
5175
|
+
* whose verification method is a DID URL for its DID, even if it is the only
|
|
5176
|
+
* key; if no key or more than one key qualifies, this function throws.
|
|
5177
|
+
*
|
|
4840
5178
|
* Outside the map-local compound-proof profile, i.e., when the activity
|
|
4841
5179
|
* embeds no portable object, every Ed25519 key signs the activity, as it
|
|
4842
5180
|
* always has. Inside the profile, which accepts exactly one direct proof per
|
|
4843
5181
|
* map, Fedify creates at most one proof:
|
|
4844
5182
|
*
|
|
4845
5183
|
* - An activity that already carries a proof is kept as is.
|
|
4846
|
-
* - A single Ed25519 key signs
|
|
4847
|
-
* - With several Ed25519 keys, a portable activity is
|
|
4848
|
-
*
|
|
4849
|
-
* proof the FEP-ef61 policy can accept. If no key or more than one key
|
|
4850
|
-
* qualifies, or the activity is not portable itself, the choice is
|
|
4851
|
-
* ambiguous and this function throws.
|
|
5184
|
+
* - A single Ed25519 key signs a non-portable activity.
|
|
5185
|
+
* - With several Ed25519 keys, a non-portable activity is ambiguous, and
|
|
5186
|
+
* this function throws.
|
|
4852
5187
|
*
|
|
4853
|
-
* @throws {TypeError} If a single proof key cannot be chosen
|
|
5188
|
+
* @throws {TypeError} If a single proof key cannot be chosen, or the activity
|
|
5189
|
+
* is performed by a portable actor but is not a portable
|
|
5190
|
+
* activity of the same DID.
|
|
4854
5191
|
* @internal
|
|
4855
5192
|
*/
|
|
4856
5193
|
async function signOutgoingActivity(activity, candidates, options) {
|
|
4857
5194
|
const { contextLoader, tracerProvider } = options;
|
|
4858
|
-
|
|
4859
|
-
for await (const _ of activity.getProofs({ contextLoader })) {
|
|
4860
|
-
hasProof = true;
|
|
4861
|
-
break;
|
|
4862
|
-
}
|
|
4863
|
-
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519");
|
|
4864
|
-
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
4865
|
-
activity,
|
|
4866
|
-
hasProof,
|
|
4867
|
-
proofCreated: false
|
|
4868
|
-
};
|
|
5195
|
+
assertPortableActorActivity(activity);
|
|
4869
5196
|
const sign = async (selected) => {
|
|
4870
5197
|
for (const { verificationMethod, privateKey } of selected) activity = await signObject(activity, privateKey, verificationMethod, {
|
|
4871
5198
|
contextLoader,
|
|
@@ -4877,6 +5204,18 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
4877
5204
|
proofCreated: true
|
|
4878
5205
|
};
|
|
4879
5206
|
};
|
|
5207
|
+
let hasProof = false;
|
|
5208
|
+
for await (const _ of activity.getProofs({ contextLoader })) {
|
|
5209
|
+
hasProof = true;
|
|
5210
|
+
break;
|
|
5211
|
+
}
|
|
5212
|
+
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isCompatibleKeyId(c.verificationMethod));
|
|
5213
|
+
if (!hasProof && activity.id != null && isPortableUrl(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
5214
|
+
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
5215
|
+
activity,
|
|
5216
|
+
hasProof,
|
|
5217
|
+
proofCreated: false
|
|
5218
|
+
};
|
|
4880
5219
|
if (keys.length < 2 && !hasProof) return await sign(keys);
|
|
4881
5220
|
if (!containsCompoundPortableObject(await activity.toJsonLd({
|
|
4882
5221
|
format: "compact",
|
|
@@ -4896,15 +5235,26 @@ async function signOutgoingActivity(activity, candidates, options) {
|
|
|
4896
5235
|
proofCreated: false
|
|
4897
5236
|
};
|
|
4898
5237
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
5238
|
+
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.`);
|
|
5239
|
+
}
|
|
5240
|
+
/**
|
|
5241
|
+
* Chooses the one Ed25519 key that can sign a portable activity: the key
|
|
5242
|
+
* whose verification method is a DID URL for the activity's DID.
|
|
5243
|
+
* @throws {TypeError} If no key or more than one key qualifies.
|
|
5244
|
+
*/
|
|
5245
|
+
function selectPortableActivityKey(activity, activityId, keys) {
|
|
5246
|
+
const activityDid = getFe34Origin(activityId);
|
|
5247
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && haveSameFe34Origin(activityId, c.verificationMethod));
|
|
5248
|
+
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.`);
|
|
5249
|
+
getLogger([
|
|
5250
|
+
"fedify",
|
|
5251
|
+
"federation",
|
|
5252
|
+
"outbox"
|
|
5253
|
+
]).debug("The portable activity {activityId} is signed only by {keyId}, the one Ed25519 key whose verification method matches its DID.", {
|
|
5254
|
+
activityId: activity.id?.href,
|
|
4905
5255
|
keyId: eligible[0].verificationMethod.href
|
|
4906
5256
|
});
|
|
4907
|
-
return
|
|
5257
|
+
return eligible;
|
|
4908
5258
|
}
|
|
4909
5259
|
/**
|
|
4910
5260
|
* Rejects an outgoing activity whose proofs Fedify's own inbox would reject
|
|
@@ -5692,6 +6042,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5692
6042
|
kvPrefixes;
|
|
5693
6043
|
publicKeyTtl;
|
|
5694
6044
|
httpMessageSignaturesSpecTtl;
|
|
6045
|
+
portableInboxForwarding;
|
|
5695
6046
|
inboxQueue;
|
|
5696
6047
|
outboxQueue;
|
|
5697
6048
|
fanoutQueue;
|
|
@@ -5724,6 +6075,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5724
6075
|
benchmarkMetricReader;
|
|
5725
6076
|
benchmarkTriggerOptions;
|
|
5726
6077
|
#mediaUploaderNoAuthWarned = false;
|
|
6078
|
+
#portableInboxForwardingWarned = false;
|
|
5727
6079
|
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
5728
6080
|
#queueDepthGaugeEntries = [];
|
|
5729
6081
|
#queueDepthGaugeMeterProvider;
|
|
@@ -5755,10 +6107,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
5755
6107
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
5756
6108
|
circuitBreaker: ["_fedify", "circuit"],
|
|
5757
6109
|
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
6110
|
+
portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
|
|
5758
6111
|
...options.kvPrefixes ?? {}
|
|
5759
6112
|
};
|
|
5760
6113
|
this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
|
|
5761
6114
|
this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
|
|
6115
|
+
this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
|
|
5762
6116
|
if (options.queue == null) {
|
|
5763
6117
|
this.inboxQueue = void 0;
|
|
5764
6118
|
this.outboxQueue = void 0;
|
|
@@ -6428,8 +6782,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6428
6782
|
]);
|
|
6429
6783
|
const baseUrl = new URL(message.baseUrl);
|
|
6430
6784
|
let context = this.#createContext(baseUrl, ctxData);
|
|
6431
|
-
if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
6432
|
-
else if (this.sharedInboxKeyDispatcher != null) {
|
|
6785
|
+
if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
6786
|
+
else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
|
|
6433
6787
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
6434
6788
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
6435
6789
|
}
|
|
@@ -6788,9 +7142,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6788
7142
|
const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
|
|
6789
7143
|
const activityId = activity.id.href;
|
|
6790
7144
|
let rsaKey = null;
|
|
7145
|
+
const ldSignable = !hasPortableActor(activity);
|
|
6791
7146
|
for (const { keyId, privateKey } of keys) {
|
|
6792
7147
|
validateCryptoKey(privateKey, "private");
|
|
6793
|
-
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKey = {
|
|
7148
|
+
if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !isCompatibleKeyId(keyId)) rsaKey = {
|
|
6794
7149
|
keyId,
|
|
6795
7150
|
privateKey
|
|
6796
7151
|
};
|
|
@@ -6811,7 +7166,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
6811
7166
|
});
|
|
6812
7167
|
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
|
|
6813
7168
|
assertSupportedCompoundProofShape(jsonLd, activityId);
|
|
6814
|
-
if (rsaKey == null) logger.
|
|
7169
|
+
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 });
|
|
7170
|
+
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.", {
|
|
6815
7171
|
activityId,
|
|
6816
7172
|
keys: keys.map((pair) => ({
|
|
6817
7173
|
keyId: pair.keyId.href,
|
|
@@ -7020,6 +7376,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7020
7376
|
span,
|
|
7021
7377
|
metricState
|
|
7022
7378
|
});
|
|
7379
|
+
if (request.method === "POST") {
|
|
7380
|
+
const portable = parsePortableGatewayRequest(url);
|
|
7381
|
+
if (portable != null) return await this.#fetchPortableInbox(request, portable, {
|
|
7382
|
+
onNotFound,
|
|
7383
|
+
contextData,
|
|
7384
|
+
span,
|
|
7385
|
+
metricState
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7023
7388
|
const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
|
|
7024
7389
|
if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
|
|
7025
7390
|
onNotFound,
|
|
@@ -7171,35 +7536,17 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7171
7536
|
onNotFound
|
|
7172
7537
|
});
|
|
7173
7538
|
context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
|
|
7174
|
-
case "sharedInbox":
|
|
7539
|
+
case "sharedInbox":
|
|
7175
7540
|
if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
|
|
7176
7541
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
7177
7542
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
7178
7543
|
}
|
|
7179
|
-
|
|
7180
|
-
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
7181
|
-
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
7182
|
-
return await handleInbox(request, {
|
|
7544
|
+
return await this.#handleInbox(request, {
|
|
7183
7545
|
recipient: route.values.identifier ?? null,
|
|
7184
7546
|
context,
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
kvPrefixes: this.kvPrefixes,
|
|
7188
|
-
publicKeyTtl: this.publicKeyTtl,
|
|
7189
|
-
queue: this.inboxQueue,
|
|
7190
|
-
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
7191
|
-
inboxListeners: this.inboxListeners,
|
|
7192
|
-
inboxErrorHandler: this.inboxErrorHandler,
|
|
7193
|
-
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
7194
|
-
onNotFound,
|
|
7195
|
-
signatureTimeWindow: this.signatureTimeWindow,
|
|
7196
|
-
skipSignatureVerification: this.skipSignatureVerification,
|
|
7197
|
-
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
7198
|
-
meterProvider: this.meterProvider,
|
|
7199
|
-
tracerProvider: this.tracerProvider,
|
|
7200
|
-
idempotencyStrategy: this.idempotencyStrategy
|
|
7547
|
+
contextData,
|
|
7548
|
+
onNotFound
|
|
7201
7549
|
});
|
|
7202
|
-
}
|
|
7203
7550
|
case "following": return await handleCollection(request, {
|
|
7204
7551
|
name: "following",
|
|
7205
7552
|
identifier: route.values.identifier,
|
|
@@ -7352,6 +7699,113 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
7352
7699
|
onNotFound
|
|
7353
7700
|
});
|
|
7354
7701
|
}
|
|
7702
|
+
async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox }) {
|
|
7703
|
+
if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
|
|
7704
|
+
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
7705
|
+
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
7706
|
+
return await handleInbox(request, {
|
|
7707
|
+
recipient,
|
|
7708
|
+
context,
|
|
7709
|
+
inboxContextFactory,
|
|
7710
|
+
kv: this.kv,
|
|
7711
|
+
kvPrefixes: this.kvPrefixes,
|
|
7712
|
+
publicKeyTtl: this.publicKeyTtl,
|
|
7713
|
+
queue: this.inboxQueue,
|
|
7714
|
+
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
7715
|
+
inboxListeners: this.inboxListeners,
|
|
7716
|
+
inboxErrorHandler: this.inboxErrorHandler,
|
|
7717
|
+
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
7718
|
+
onNotFound,
|
|
7719
|
+
signatureTimeWindow: this.signatureTimeWindow,
|
|
7720
|
+
skipSignatureVerification: this.skipSignatureVerification,
|
|
7721
|
+
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
7722
|
+
meterProvider: this.meterProvider,
|
|
7723
|
+
tracerProvider: this.tracerProvider,
|
|
7724
|
+
idempotencyStrategy: this.idempotencyStrategy,
|
|
7725
|
+
portableInbox
|
|
7726
|
+
});
|
|
7727
|
+
}
|
|
7728
|
+
async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
|
|
7729
|
+
const logger = getLogger([
|
|
7730
|
+
"fedify",
|
|
7731
|
+
"federation",
|
|
7732
|
+
"inbox"
|
|
7733
|
+
]);
|
|
7734
|
+
if (portable.type === "malformed") {
|
|
7735
|
+
metricState.endpoint = "not_found";
|
|
7736
|
+
logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
|
|
7737
|
+
url: request.url,
|
|
7738
|
+
error: portable.error
|
|
7739
|
+
});
|
|
7740
|
+
return new Response("Malformed portable inbox ID.", {
|
|
7741
|
+
status: 400,
|
|
7742
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
7743
|
+
});
|
|
7744
|
+
}
|
|
7745
|
+
const route = this.router.route(portable.path);
|
|
7746
|
+
if (route == null || route.name !== "inbox") {
|
|
7747
|
+
metricState.endpoint = "not_found";
|
|
7748
|
+
return await onNotFound(request);
|
|
7749
|
+
}
|
|
7750
|
+
metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
|
|
7751
|
+
metricState.endpoint = "inbox";
|
|
7752
|
+
span.updateName(`${request.method} ${metricState.routeTemplate}`);
|
|
7753
|
+
const identifier = route.values.identifier;
|
|
7754
|
+
let context = this.#createContext(request, contextData);
|
|
7755
|
+
const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
|
|
7756
|
+
const localOrigin = context.canonicalOrigin;
|
|
7757
|
+
const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
|
|
7758
|
+
authority: portable.portableRequest.authority,
|
|
7759
|
+
canonicalInboxId: portable.canonicalId,
|
|
7760
|
+
localOrigin
|
|
7761
|
+
});
|
|
7762
|
+
if (resolution.status === "rejected") {
|
|
7763
|
+
logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
|
|
7764
|
+
inbox: portable.canonicalId,
|
|
7765
|
+
identifier,
|
|
7766
|
+
reason: resolution.reason
|
|
7767
|
+
});
|
|
7768
|
+
return await onNotFound(request);
|
|
7769
|
+
}
|
|
7770
|
+
const { recipient } = resolution;
|
|
7771
|
+
if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
|
|
7772
|
+
const excludedOrigins = [localOrigin, new URL(request.url).origin];
|
|
7773
|
+
return await this.#handleInbox(request, {
|
|
7774
|
+
recipient: identifier,
|
|
7775
|
+
context,
|
|
7776
|
+
contextData,
|
|
7777
|
+
onNotFound,
|
|
7778
|
+
portableInbox: {
|
|
7779
|
+
recipient,
|
|
7780
|
+
forward: async (activity, activityId, activityType) => {
|
|
7781
|
+
if (this.portableInboxForwarding.maxTargets < 1) return;
|
|
7782
|
+
if (this.kv.cas == null) {
|
|
7783
|
+
if (!this.#portableInboxForwardingWarned) {
|
|
7784
|
+
this.#portableInboxForwardingWarned = true;
|
|
7785
|
+
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.");
|
|
7786
|
+
}
|
|
7787
|
+
return;
|
|
7788
|
+
}
|
|
7789
|
+
await forwardPortableInboxActivity({
|
|
7790
|
+
recipient,
|
|
7791
|
+
activity,
|
|
7792
|
+
activityId,
|
|
7793
|
+
activityType,
|
|
7794
|
+
excludedOrigins,
|
|
7795
|
+
baseUrl: context.origin,
|
|
7796
|
+
kv: this.kv,
|
|
7797
|
+
kvPrefix: this.kvPrefixes.portableInboxForwarding,
|
|
7798
|
+
outboxQueue: this.outboxQueue,
|
|
7799
|
+
startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
|
|
7800
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
7801
|
+
options: this.portableInboxForwarding,
|
|
7802
|
+
meterProvider: this.meterProvider,
|
|
7803
|
+
tracerProvider: this.tracerProvider
|
|
7804
|
+
});
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
});
|
|
7808
|
+
}
|
|
7355
7809
|
async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
|
|
7356
7810
|
metricState.endpoint = "hashlink_media";
|
|
7357
7811
|
metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
|
|
@@ -7582,6 +8036,12 @@ var ContextImpl = class ContextImpl {
|
|
|
7582
8036
|
if (path == null) throw new RouterError("No inbox path registered.");
|
|
7583
8037
|
return new URL(path, this.canonicalOrigin);
|
|
7584
8038
|
}
|
|
8039
|
+
getPortableInboxUri(identifier, authority) {
|
|
8040
|
+
const path = this.federation.router.build("inbox", { identifier });
|
|
8041
|
+
if (path == null) throw new RouterError("No inbox path registered.");
|
|
8042
|
+
if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
|
|
8043
|
+
return buildPortableUri(authority, path);
|
|
8044
|
+
}
|
|
7585
8045
|
getFollowingUri(identifier) {
|
|
7586
8046
|
const path = this.federation.router.build("following", { identifier });
|
|
7587
8047
|
if (path == null) throw new RouterError("No following collection path registered.");
|
|
@@ -7687,14 +8147,15 @@ var ContextImpl = class ContextImpl {
|
|
|
7687
8147
|
getActorKeyPairsIdentifier: identifier,
|
|
7688
8148
|
actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
|
|
7689
8149
|
});
|
|
8150
|
+
const portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
7690
8151
|
let keyPairs;
|
|
7691
8152
|
try {
|
|
7692
|
-
keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
|
8153
|
+
keyPairs = await this.getKeyPairsFromIdentifier(identifier, portableActor);
|
|
7693
8154
|
} catch (_) {
|
|
7694
8155
|
logger.warn("No actor key pairs dispatcher registered.");
|
|
7695
8156
|
return [];
|
|
7696
8157
|
}
|
|
7697
|
-
const owner = this.getActorUri(identifier);
|
|
8158
|
+
const owner = portableActor?.id ?? this.getActorUri(identifier);
|
|
7698
8159
|
const result = [];
|
|
7699
8160
|
let i = 1;
|
|
7700
8161
|
for (const keyPair of keyPairs) {
|
|
@@ -7706,7 +8167,7 @@ var ContextImpl = class ContextImpl {
|
|
|
7706
8167
|
publicKey: keyPair.publicKey
|
|
7707
8168
|
}),
|
|
7708
8169
|
multikey: new Multikey({
|
|
7709
|
-
id: new URL(`#multikey-${i}`, owner),
|
|
8170
|
+
id: portableActor == null ? new URL(`#multikey-${i}`, owner) : keyPair.keyId,
|
|
7710
8171
|
controller: owner,
|
|
7711
8172
|
publicKey: keyPair.publicKey
|
|
7712
8173
|
})
|
|
@@ -7716,7 +8177,26 @@ var ContextImpl = class ContextImpl {
|
|
|
7716
8177
|
}
|
|
7717
8178
|
return result;
|
|
7718
8179
|
}
|
|
7719
|
-
|
|
8180
|
+
/**
|
|
8181
|
+
* Resolves the portable actor that the actor with the given identifier is,
|
|
8182
|
+
* through the portable actor ID mapper, along with the base of its gateway
|
|
8183
|
+
* key IDs on this server.
|
|
8184
|
+
* @param identifier The actor's identifier.
|
|
8185
|
+
* @returns The portable actor's ID and key ID base, or `null` if the actor
|
|
8186
|
+
* is not portable.
|
|
8187
|
+
* @throws {TypeError} If the mapper returns an invalid portable actor ID.
|
|
8188
|
+
*/
|
|
8189
|
+
async getPortableActorKeyOwner(identifier) {
|
|
8190
|
+
const mapper = this.federation.actorCallbacks?.portableActorIdMapper;
|
|
8191
|
+
if (mapper == null) return null;
|
|
8192
|
+
const id = await mapper(this, identifier);
|
|
8193
|
+
if (id == null) return null;
|
|
8194
|
+
return {
|
|
8195
|
+
id,
|
|
8196
|
+
keyBase: getGatewayKeyBase(id, this.canonicalOrigin)
|
|
8197
|
+
};
|
|
8198
|
+
}
|
|
8199
|
+
async getKeyPairsFromIdentifier(identifier, portableActor) {
|
|
7720
8200
|
const logger = getLogger([
|
|
7721
8201
|
"fedify",
|
|
7722
8202
|
"federation",
|
|
@@ -7733,6 +8213,8 @@ var ContextImpl = class ContextImpl {
|
|
|
7733
8213
|
}
|
|
7734
8214
|
throw error;
|
|
7735
8215
|
}
|
|
8216
|
+
if (portableActor === void 0) portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
8217
|
+
const keyBase = portableActor?.keyBase ?? actorUri;
|
|
7736
8218
|
const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
|
|
7737
8219
|
...this,
|
|
7738
8220
|
invokedFromActorKeyPairsDispatcher: { identifier }
|
|
@@ -7743,7 +8225,7 @@ var ContextImpl = class ContextImpl {
|
|
|
7743
8225
|
for (const keyPair of keyPairs) {
|
|
7744
8226
|
result.push({
|
|
7745
8227
|
...keyPair,
|
|
7746
|
-
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`,
|
|
8228
|
+
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, keyBase)
|
|
7747
8229
|
});
|
|
7748
8230
|
i++;
|
|
7749
8231
|
}
|
|
@@ -8162,6 +8644,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
8162
8644
|
getPortableObjectUri(cls, values, authority) {
|
|
8163
8645
|
return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
|
|
8164
8646
|
}
|
|
8647
|
+
getPortableInboxUri(identifier, authority) {
|
|
8648
|
+
return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
|
|
8649
|
+
}
|
|
8165
8650
|
async getActor(identifier, options) {
|
|
8166
8651
|
if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
|
|
8167
8652
|
if (this.#invokedFromActorDispatcher != null) getLogger([
|