@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271
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/README.md +50 -39
- package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
- package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
- package/dist/assert_exists-cffSpJ7X.mjs +28 -0
- package/dist/assert_false-CPdRnc3E.mjs +23 -0
- package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
- package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
- package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
- package/dist/assert_rejects-CTptdYck.mjs +39 -0
- package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
- package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
- package/dist/assertion_error-B-cx0fFA.mjs +31 -0
- package/dist/audience-CHQ-FlLD.mjs +95 -0
- package/dist/{builder-DonuxRl_.mjs → builder-Bda6LK7A.mjs} +169 -14
- package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
- package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
- package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
- package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
- package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
- package/dist/compat/mod.cjs +1 -1
- package/dist/compat/mod.d.cts +1 -1
- package/dist/compat/mod.d.ts +1 -1
- package/dist/compat/mod.js +1 -1
- package/dist/compat/outgoing-jsonld.test.mjs +71 -3
- package/dist/compat/public-audience.test.mjs +14 -5
- package/dist/compat/secured-document.test.d.mts +2 -0
- package/dist/compat/secured-document.test.mjs +58 -0
- package/dist/compat/transformers.test.mjs +5 -5
- package/dist/compound-proof-B-9mXxFW.mjs +989 -0
- package/dist/{context-Zii1f5xS.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-lxX6I7as.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-BHzKn9Ig.mjs → deno-oR5aJKpU.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-DBoYbvYW.mjs +87 -0
- package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
- package/dist/federation/audience.test.d.mts +2 -0
- package/dist/federation/audience.test.mjs +130 -0
- package/dist/federation/builder.test.mjs +54 -5
- package/dist/federation/circuit-breaker.test.mjs +838 -7
- package/dist/federation/collection.test.mjs +3 -3
- package/dist/federation/compound-fanout.test.d.mts +2 -0
- package/dist/federation/compound-fanout.test.mjs +228 -0
- package/dist/federation/handler-compound-proof.test.d.mts +2 -0
- package/dist/federation/handler-compound-proof.test.mjs +384 -0
- package/dist/federation/handler.test.mjs +385 -15
- package/dist/federation/hashlink-media.test.d.mts +2 -0
- package/dist/federation/hashlink-media.test.mjs +422 -0
- package/dist/federation/idempotency.test.mjs +6 -6
- package/dist/federation/inbox-report.test.d.mts +2 -0
- package/dist/federation/inbox-report.test.mjs +475 -0
- package/dist/federation/inbox.test.mjs +3 -3
- package/dist/federation/keycache.test.mjs +215 -14
- package/dist/federation/kv.test.mjs +3 -3
- package/dist/federation/metrics.test.mjs +5 -5
- package/dist/federation/middleware.test.mjs +967 -35
- package/dist/federation/mod.cjs +18 -261
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +279 -1
- package/dist/federation/mq.test.mjs +123 -47
- package/dist/federation/negotiation.test.mjs +4 -4
- package/dist/federation/outgoing-proof.test.d.mts +2 -0
- package/dist/federation/outgoing-proof.test.mjs +931 -0
- package/dist/federation/portable-actor.test.d.mts +2 -0
- package/dist/federation/portable-actor.test.mjs +496 -0
- package/dist/federation/portable-chain.test.d.mts +2 -0
- package/dist/federation/portable-chain.test.mjs +370 -0
- package/dist/federation/portable-collection.test.d.mts +2 -0
- package/dist/federation/portable-collection.test.mjs +832 -0
- package/dist/federation/portable-delivery.test.d.mts +2 -0
- package/dist/federation/portable-delivery.test.mjs +746 -0
- package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
- package/dist/federation/portable-gateway-keys.test.mjs +686 -0
- package/dist/federation/portable-inbox.test.d.mts +2 -0
- package/dist/federation/portable-inbox.test.mjs +1535 -0
- package/dist/federation/portable-parse-uri.test.d.mts +2 -0
- package/dist/federation/portable-parse-uri.test.mjs +348 -0
- package/dist/federation/portable-route-activity.test.d.mts +2 -0
- package/dist/federation/portable-route-activity.test.mjs +259 -0
- package/dist/federation/portable.test.d.mts +2 -0
- package/dist/federation/portable.test.mjs +1072 -0
- package/dist/federation/retry.test.mjs +4 -3
- package/dist/federation/router.test.mjs +9 -159
- package/dist/federation/send.test.mjs +245 -27
- package/dist/federation/tasks/codec.test.mjs +4 -4
- package/dist/federation/tasks/enqueue.test.mjs +42 -4
- package/dist/federation/tasks/tasks.test.mjs +7 -6
- package/dist/federation/temporal.test.mjs +4 -4
- package/dist/federation/webfinger.test.mjs +275 -12
- package/dist/{middleware-FrSMAYEx.cjs → federation-CkZiAcWY.cjs} +8640 -3722
- package/dist/http-B5JT115n.cjs +10630 -0
- package/dist/http-DydWCB7R.js +6527 -0
- package/dist/{http-CPVMUgsW.mjs → http-siciWkmx.mjs} +397 -168
- package/dist/key-BspPiXqn.mjs +951 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-DYxuyd-s.d.cts +1125 -0
- package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
- package/dist/keycache-Da8A8mZa.mjs +200 -0
- package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
- package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
- package/dist/kv-DBRWVx3Q.mjs +104 -0
- package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-BF1hnW65.mjs} +1 -1
- package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-CTQr1tbn.js} +54 -16
- package/dist/{kv-cache-DQ29C4XL.cjs → kv-cache-DCac1nEU.cjs} +55 -19
- package/dist/{ld-Cj9F_5AP.mjs → ld-CzaP-hJl.mjs} +44 -12
- package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
- package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
- package/dist/{metrics-DMk-nTxI.mjs → metrics-CYg9yxBL.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-Y-kpsp6S.js → middleware-CsDDXKuL.js} +5493 -1103
- package/dist/{middleware-CvwTUgnP.mjs → middleware-D7_LO-Vs.mjs} +3157 -272
- package/dist/{middleware-CEctxBN6.mjs → middleware-DWjrfpcz.mjs} +1 -1
- package/dist/{mod-gOlPDuT8.d.cts → mod-BojP2Bw3.d.cts} +1 -1
- package/dist/mod-CavhAgLw.d.cts +320 -0
- package/dist/mod-CtpF2BU5.d.ts +320 -0
- package/dist/{mod-BtEaIEcD.d.cts → mod-DEZXwV6J.d.ts} +10 -4
- package/dist/{mod-UMmveiO0.d.ts → mod-Dw-nPxQh.d.cts} +10 -4
- package/dist/{mod-DLc2Ot6f.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
- package/dist/mod.cjs +40 -39
- package/dist/mod.d.cts +10 -11
- package/dist/mod.d.ts +10 -11
- package/dist/mod.js +9 -9
- package/dist/{mq-llHKivST.mjs → mq-Dm1Wmtm-.mjs} +6 -0
- package/dist/nodeinfo/client.test.mjs +139 -4
- package/dist/nodeinfo/handler.test.mjs +5 -5
- package/dist/nodeinfo/mod.cjs +1 -1
- package/dist/nodeinfo/mod.d.cts +1 -1
- package/dist/nodeinfo/mod.d.ts +1 -1
- package/dist/nodeinfo/mod.js +1 -1
- package/dist/nodeinfo/types.test.mjs +4 -4
- package/dist/otel/exporter.test.mjs +3 -3
- package/dist/otel/mod.cjs +0 -1
- package/dist/otel/mod.d.cts +1 -1
- package/dist/otel/mod.d.ts +1 -1
- package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
- package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
- package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
- package/dist/owner-DYgfTFHy.mjs +267 -0
- package/dist/portable-collection-BpsVsRov.js +670 -0
- package/dist/portable-collection-C74P1vtC.mjs +418 -0
- package/dist/portable-collection-DZd2bm7M.cjs +686 -0
- package/dist/portable-key-BIMLrAP4.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-Dxy6ByqI.mjs +1299 -0
- package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
- package/dist/router-u5O5KCQr.mjs +156 -0
- package/dist/runtime/mod.cjs +1 -1
- package/dist/runtime/mod.js +1 -1
- package/dist/secured-document-CxzZZ9nL.mjs +35 -0
- package/dist/send-C4dDpAxm.mjs +495 -0
- package/dist/sig/accept.test.mjs +1 -1
- package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
- package/dist/sig/compound-proof-discovery.test.mjs +331 -0
- package/dist/sig/compound-proof-producer.test.d.mts +2 -0
- package/dist/sig/compound-proof-producer.test.mjs +107 -0
- package/dist/sig/compound-proof-verification.test.d.mts +2 -0
- package/dist/sig/compound-proof-verification.test.mjs +833 -0
- package/dist/sig/compound-proof.test.d.mts +2 -0
- package/dist/sig/compound-proof.test.mjs +411 -0
- package/dist/sig/http.test.mjs +702 -31
- package/dist/sig/key-owner-evidence.test.d.mts +2 -0
- package/dist/sig/key-owner-evidence.test.mjs +100 -0
- package/dist/sig/key.test.mjs +220 -8
- package/dist/sig/ld.test.mjs +65 -7
- package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
- package/dist/sig/mitra-receive-interop.test.mjs +260 -0
- package/dist/sig/mitra-send-interop.test.d.mts +2 -0
- package/dist/sig/mitra-send-interop.test.mjs +461 -0
- package/dist/sig/mod.cjs +18 -16
- package/dist/sig/mod.d.cts +4 -4
- package/dist/sig/mod.d.ts +4 -4
- package/dist/sig/mod.js +3 -3
- package/dist/sig/owner.test.mjs +108 -9
- package/dist/sig/portable-collection.test.d.mts +2 -0
- package/dist/sig/portable-collection.test.mjs +697 -0
- package/dist/sig/portable-interop.test.d.mts +2 -0
- package/dist/sig/portable-interop.test.mjs +156 -0
- package/dist/sig/portable-key-id.test.d.mts +2 -0
- package/dist/sig/portable-key-id.test.mjs +67 -0
- package/dist/sig/portable-key.test.d.mts +2 -0
- package/dist/sig/portable-key.test.mjs +1439 -0
- package/dist/sig/proof.test.mjs +1753 -16
- package/dist/sig/tootik-request-interop.test.d.mts +2 -0
- package/dist/sig/tootik-request-interop.test.mjs +190 -0
- package/dist/sig/tootik-send-interop.test.d.mts +2 -0
- package/dist/sig/tootik-send-interop.test.mjs +331 -0
- package/dist/sig/verification.test.d.mts +2 -0
- package/dist/sig/verification.test.mjs +310 -0
- package/dist/std__assert-DvMDRPes.mjs +8 -0
- package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
- package/dist/{temporal-fsXO5IvD.mjs → temporal-D0KJI4NF.mjs} +3 -3
- package/dist/testing/context.test.d.mts +2 -0
- package/dist/testing/context.test.mjs +19 -0
- package/dist/testing/kv.test.d.mts +2 -0
- package/dist/testing/kv.test.mjs +101 -0
- package/dist/testing/mod.d.mts +1617 -48
- package/dist/testing/mod.mjs +6 -3
- package/dist/testing-t8xDeJZ5.mjs +8 -0
- package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
- package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
- package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
- package/dist/utils/docloader.test.mjs +530 -8
- package/dist/utils/kv-cache.test.mjs +2 -2
- 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/dist/verification-C3954hQh.mjs +5 -0
- package/dist/verification-Dpfcw8ml.mjs +105 -0
- package/dist/vocab/cjs.test.mjs +1 -1
- package/package.json +8 -8
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-e1khNUKy.mjs +0 -49
- package/dist/http-CsmQ-BQ4.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-hlIjmewS.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-DfPsX8hv.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-CoUJLQhZ.d.cts +0 -276
- package/dist/metrics-Dc4jR-IC.d.ts +0 -276
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Br9YYzn_.mjs +0 -120
- package/dist/proof-B4ngoaNQ.mjs +0 -316
- package/dist/proof-B6E0XUXJ.js +0 -1415
- package/dist/proof-NN4xCQGL.cjs +0 -1562
- package/dist/send-CxHKUNzv.mjs +0 -239
- package/dist/std__assert-B2NnY9bq.mjs +0 -79
- package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
- /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
- /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
- /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
- /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
- /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
- /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
- /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
- /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
- /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
- /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
- /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
- /package/dist/{types-CrtdVuDP.mjs → types-DE_LV1JB.mjs} +0 -0
|
@@ -1,39 +1,150 @@
|
|
|
1
1
|
import { Temporal } from "temporal-polyfill";
|
|
2
2
|
import "urlpattern-polyfill";
|
|
3
3
|
globalThis.addEventListener = () => {};
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
8
|
-
import { l as
|
|
9
|
-
import { t as
|
|
10
|
-
import { n as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { t as
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { t as
|
|
20
|
-
import { t as
|
|
21
|
-
import { t as
|
|
22
|
-
import { t as
|
|
23
|
-
import { t as
|
|
24
|
-
import { t as
|
|
25
|
-
import { t as
|
|
26
|
-
import {
|
|
4
|
+
import { d as isPortableUri, f as isSameObjectId, i as getPortableDid, l as isPortableId, n as getCanonicalPortableId, o as hasPortableActor, r as getGatewayKeyBase, s as isCompatibleKeyId, t as getAuthenticationOrigin, u as isPortableKeyId } from "./portable-key-id-Ca30-hOa.mjs";
|
|
5
|
+
import { n as isPubliclyAddressedNode, t as isInAudience } from "./audience-CHQ-FlLD.mjs";
|
|
6
|
+
import { a as verificationObservation, r as snapshotKey, t as observeAttempt } from "./verification-Dpfcw8ml.mjs";
|
|
7
|
+
import { n as version, t as name } from "./deno-oR5aJKpU.mjs";
|
|
8
|
+
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-CYg9yxBL.mjs";
|
|
9
|
+
import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
|
|
10
|
+
import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-BspPiXqn.mjs";
|
|
11
|
+
import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-4IJqPYos.mjs";
|
|
12
|
+
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-CzaP-hJl.mjs";
|
|
13
|
+
import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-Dxy6ByqI.mjs";
|
|
14
|
+
import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-siciWkmx.mjs";
|
|
15
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-DBoYbvYW.mjs";
|
|
16
|
+
import { n as kvCache } from "./kv-cache-BF1hnW65.mjs";
|
|
17
|
+
import { d as readBoundedText, l as BodyTooLargeError, t as getNodeInfo, u as MAX_BODY_SIZE } from "./client-CEnNz8O7.mjs";
|
|
18
|
+
import { t as nodeInfoToJson } from "./types-DE_LV1JB.mjs";
|
|
19
|
+
import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-DYgfTFHy.mjs";
|
|
20
|
+
import { t as verifyPortableObject } from "./portable-collection-C74P1vtC.mjs";
|
|
21
|
+
import { a as getOrderingDestination, i as sendActivityThroughGateways, n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-C4dDpAxm.mjs";
|
|
22
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-Bda6LK7A.mjs";
|
|
23
|
+
import { t as CircuitBreaker } from "./circuit-breaker-XJ53md4S.mjs";
|
|
24
|
+
import { t as buildCollectionSynchronizationHeader } from "./collection-CnnP1UAv.mjs";
|
|
25
|
+
import { a as inspectCompoundPortableObjectApplicability, c as verifyServedPortableObjects, i as findUnsupportedCompoundProofShape, o as verifyCompoundPortableObjectProofs, r as findEmbeddedProofWithoutContext, t as containsCompoundPortableObject } from "./compound-proof-B-9mXxFW.mjs";
|
|
26
|
+
import { t as KvKeyCache } from "./keycache-Da8A8mZa.mjs";
|
|
27
|
+
import { t as acceptsJsonLd } from "./negotiation-CCocWT_N.mjs";
|
|
28
|
+
import { t as hasMalformedKnownTemporalLiteral } from "./temporal-D0KJI4NF.mjs";
|
|
29
|
+
import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
|
|
30
|
+
import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
|
|
27
31
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
28
32
|
import { RouterError } from "@fedify/uri-template";
|
|
29
|
-
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
33
|
+
import { Activity, Collection, CollectionPage, CryptographicKey, Link, Multikey, Object as Object$1, OrderedCollection, OrderedCollectionPage, Tombstone, getTypeId, isActor, lookupObject, traverseCollection } from "@fedify/vocab";
|
|
30
34
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
31
35
|
import { SpanKind, SpanStatusCode, context, metrics, propagation, trace } from "@opentelemetry/api";
|
|
36
|
+
import { FetchError, canonicalizePortableUri, decodeMultibase, formatIri, fromCompatibleEf61Id, getDocumentLoader, getFe34Origin, isGatewayUrl, parseDigestMultibase, parseHashlink, parseIri, resolveDocumentLoaderTimeout, toCompatibleEf61Id } from "@fedify/vocab-runtime";
|
|
37
|
+
import { isCompatibleEf61Iri } from "@fedify/vocab-runtime/internal/portable-dereference";
|
|
32
38
|
import { uniq } from "es-toolkit";
|
|
33
|
-
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
34
39
|
import { ATTR_HTTP_REQUEST_HEADER, ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_RESPONSE_HEADER, ATTR_HTTP_RESPONSE_STATUS_CODE, ATTR_URL_FULL } from "@opentelemetry/semantic-conventions";
|
|
40
|
+
import jsonld from "@fedify/vocab-runtime/jsonld";
|
|
35
41
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
36
42
|
import { domainToASCII } from "node:url";
|
|
43
|
+
//#region src/federation/inbox-observation.ts
|
|
44
|
+
/** Internal state owned by the ingress boundary, never serialized into queues. */
|
|
45
|
+
var InboxObservation = class {
|
|
46
|
+
inbox;
|
|
47
|
+
payload = { status: "unavailable" };
|
|
48
|
+
activity = null;
|
|
49
|
+
authentication = { status: "notDetermined" };
|
|
50
|
+
hasException = false;
|
|
51
|
+
stage = "prepare";
|
|
52
|
+
result = {
|
|
53
|
+
disposition: "rejected",
|
|
54
|
+
reason: "recipientNotFound"
|
|
55
|
+
};
|
|
56
|
+
verification = {
|
|
57
|
+
attempts: [],
|
|
58
|
+
subject: {
|
|
59
|
+
id: null,
|
|
60
|
+
pointer: ""
|
|
61
|
+
},
|
|
62
|
+
parsedObject: (object) => {
|
|
63
|
+
if (object instanceof Activity) {
|
|
64
|
+
this.activity = object;
|
|
65
|
+
this.verification.subject.id = object.id == null ? null : new URL(object.id.href);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
constructor(inbox) {
|
|
70
|
+
this.inbox = inbox;
|
|
71
|
+
}
|
|
72
|
+
/** Run an ingress operation and project telemetry; fetch owns completion. */
|
|
73
|
+
async run(operation, span) {
|
|
74
|
+
let response;
|
|
75
|
+
let outcome;
|
|
76
|
+
let threw = false;
|
|
77
|
+
let originalError;
|
|
78
|
+
try {
|
|
79
|
+
response = await operation();
|
|
80
|
+
outcome = {
|
|
81
|
+
type: "response",
|
|
82
|
+
status: response.status,
|
|
83
|
+
...this.result
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
threw = true;
|
|
87
|
+
this.hasException = true;
|
|
88
|
+
originalError = error;
|
|
89
|
+
outcome = {
|
|
90
|
+
type: "exception",
|
|
91
|
+
stage: this.stage,
|
|
92
|
+
error
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (span != null) this.project(span, outcome);
|
|
96
|
+
if (threw) throw originalError;
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
/** Complete standalone handler calls that have no enclosing fetch boundary. */
|
|
100
|
+
async runAndFinish(getContext, handler, operation) {
|
|
101
|
+
let response;
|
|
102
|
+
try {
|
|
103
|
+
response = await this.run(operation);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
await this.finish(getContext(), handler, {
|
|
106
|
+
type: "exception",
|
|
107
|
+
stage: this.stage,
|
|
108
|
+
error
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
await this.finish(getContext(), handler, {
|
|
113
|
+
type: "response",
|
|
114
|
+
status: response.status,
|
|
115
|
+
...this.result
|
|
116
|
+
});
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
project(span, outcome) {
|
|
120
|
+
span.setAttribute("activitypub.authentication.status", this.authentication.status);
|
|
121
|
+
if (this.authentication.status === "rejected") span.setAttribute("activitypub.verification.failure_reason", this.authentication.reason.type);
|
|
122
|
+
span.setAttribute("activitypub.inbox.disposition", outcome?.type === "exception" || this.hasException ? "exception" : this.result.disposition);
|
|
123
|
+
}
|
|
124
|
+
async finish(context, handler, outcome) {
|
|
125
|
+
if (handler == null) return;
|
|
126
|
+
const report = {
|
|
127
|
+
inbox: this.inbox,
|
|
128
|
+
payload: this.payload,
|
|
129
|
+
activity: this.activity,
|
|
130
|
+
attempts: this.verification.attempts,
|
|
131
|
+
authentication: this.authentication,
|
|
132
|
+
outcome
|
|
133
|
+
};
|
|
134
|
+
try {
|
|
135
|
+
await handler(context, report);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
try {
|
|
138
|
+
getLogger([
|
|
139
|
+
"fedify",
|
|
140
|
+
"federation",
|
|
141
|
+
"inbox"
|
|
142
|
+
]).error("An unexpected error occurred in inbox request finished handler:\n{error}", { error });
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
//#endregion
|
|
37
148
|
//#region src/compat/transformers.ts
|
|
38
149
|
const logger$2 = getLogger([
|
|
39
150
|
"fedify",
|
|
@@ -354,6 +465,180 @@ function serializeDataPointType(dataPointType) {
|
|
|
354
465
|
}
|
|
355
466
|
}
|
|
356
467
|
//#endregion
|
|
468
|
+
//#region src/federation/compatible-id-warning.ts
|
|
469
|
+
/** Returns a verified first gateway, without dereferencing the actor. */
|
|
470
|
+
async function getLocalFirstGateway(context, ownerId, did) {
|
|
471
|
+
try {
|
|
472
|
+
const parsed = context.parseUri(ownerId, { portable: true });
|
|
473
|
+
if (parsed?.type !== "actor" || parsed.authority !== did) return null;
|
|
474
|
+
return firstGateway(await context.getActor(parsed.identifier), did);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
getLogger([
|
|
477
|
+
"fedify",
|
|
478
|
+
"federation",
|
|
479
|
+
"object"
|
|
480
|
+
]).debug("Could not check the first gateway of {ownerId}: {error}", {
|
|
481
|
+
ownerId: ownerId.href,
|
|
482
|
+
error
|
|
483
|
+
});
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function firstGateway(actor, did) {
|
|
488
|
+
if (actor?.id == null || getPortableDid(actor.id) !== did) return null;
|
|
489
|
+
const gateway = actor.gateway;
|
|
490
|
+
return gateway != null && isGatewayUrl(gateway) ? gateway : null;
|
|
491
|
+
}
|
|
492
|
+
/** Looks for an actor already embedded in the document being published. */
|
|
493
|
+
function getEmbeddedFirstGateway(json, ownerId, did) {
|
|
494
|
+
const owner = getCanonicalPortableId(ownerId);
|
|
495
|
+
if (owner == null) return null;
|
|
496
|
+
return visitJsonMaps(json, (map) => {
|
|
497
|
+
const id = parseMapId(map, isJsonMap(json) ? json : null);
|
|
498
|
+
if (id == null || getCanonicalPortableId(id) !== owner) return;
|
|
499
|
+
const gateways = map.gateways ?? map["https://w3id.org/fep/ef61/gateways"];
|
|
500
|
+
const first = Array.isArray(gateways) ? gateways[0] : isJsonMap(gateways) && Array.isArray(gateways["@list"]) ? gateways["@list"][0] : null;
|
|
501
|
+
const address = typeof first === "string" ? first : isJsonMap(first) ? first.id ?? first["@id"] : null;
|
|
502
|
+
if (typeof address !== "string") return;
|
|
503
|
+
try {
|
|
504
|
+
const gateway = new URL(address);
|
|
505
|
+
if (getPortableDid(id) === did && isGatewayUrl(gateway)) return gateway;
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (!(error instanceof TypeError)) throw error;
|
|
508
|
+
}
|
|
509
|
+
}) ?? null;
|
|
510
|
+
}
|
|
511
|
+
function isJsonMap(value) {
|
|
512
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
513
|
+
}
|
|
514
|
+
/** Warns only when the publisher's actual first gateway is known. */
|
|
515
|
+
function warnCompatibleId(id, did, gateway, kind) {
|
|
516
|
+
if (!isCompatibleEf61Iri(id) || getPortableDid(id) !== did || id.origin === gateway.origin) return;
|
|
517
|
+
getLogger([
|
|
518
|
+
"fedify",
|
|
519
|
+
"federation",
|
|
520
|
+
kind === "activity" ? "outbox" : "object"
|
|
521
|
+
]).warn("The {kind} {id} has an FEP-ef61 compatible identifier on another gateway than its owner's first gateway, {gateway}. FEP-ef61 requires publishers to use the first gateway in the actor's gateways.", {
|
|
522
|
+
kind,
|
|
523
|
+
id: id.href,
|
|
524
|
+
gateway: gateway.origin
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/** Reads the root ID without dereferencing or expanding the document. */
|
|
528
|
+
function getCompactRootId(json) {
|
|
529
|
+
return isJsonMap(json) ? parseMapId(json, json) : null;
|
|
530
|
+
}
|
|
531
|
+
/** Checks an activity and its embedded objects against each one's owner. */
|
|
532
|
+
async function warnCompatibleIdsInJson(json, context) {
|
|
533
|
+
const root = isJsonMap(json) ? json : null;
|
|
534
|
+
const rootOwners = root == null ? [] : ownerIds(root, "actor", root);
|
|
535
|
+
const gateways = /* @__PURE__ */ new Map();
|
|
536
|
+
const maps = [];
|
|
537
|
+
visitJsonMaps(json, (map, path) => {
|
|
538
|
+
maps.push({
|
|
539
|
+
map,
|
|
540
|
+
path
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
for (const { map, path } of maps) {
|
|
544
|
+
const id = parseMapId(map, root);
|
|
545
|
+
if (id == null || !isCompatibleEf61Iri(id)) continue;
|
|
546
|
+
const type = map.type ?? map["@type"];
|
|
547
|
+
if ((Array.isArray(type) ? type : [type]).some((t) => typeof t === "string" && /(?:^|[#/:])(CryptographicKey|Multikey|JsonWebKey|Key)$/.test(t))) continue;
|
|
548
|
+
const did = getPortableDid(id);
|
|
549
|
+
if (did == null) continue;
|
|
550
|
+
const owners = path === "" ? rootOwners : [
|
|
551
|
+
...ownerIds(map, "actor", root),
|
|
552
|
+
...ownerIds(map, "attributedTo", root),
|
|
553
|
+
...rootOwners
|
|
554
|
+
];
|
|
555
|
+
for (const ownerId of owners) {
|
|
556
|
+
if (getPortableDid(ownerId) !== did) continue;
|
|
557
|
+
const key = getCanonicalPortableId(ownerId);
|
|
558
|
+
if (key == null) continue;
|
|
559
|
+
let gateway = gateways.get(key);
|
|
560
|
+
if (gateway === void 0) {
|
|
561
|
+
gateway = getEmbeddedFirstGateway(json, ownerId, did) ?? (context == null ? null : await getLocalFirstGateway(context, ownerId, did));
|
|
562
|
+
gateways.set(key, gateway);
|
|
563
|
+
}
|
|
564
|
+
if (gateway != null) {
|
|
565
|
+
warnCompatibleId(id, did, gateway, path === "" ? "activity" : "object");
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function ownerIds(map, term, root) {
|
|
572
|
+
const iri = `https://www.w3.org/ns/activitystreams#${term}`;
|
|
573
|
+
const values = [map[term], map[iri]];
|
|
574
|
+
for (const context of [map["@context"], root?.["@context"]]) for (const [alias, definition] of localContextEntries(context)) {
|
|
575
|
+
const mapped = typeof definition === "string" ? definition : isJsonMap(definition) ? definition["@id"] : null;
|
|
576
|
+
if (mapped === iri || mapped === `as:${term}`) values.push(map[alias]);
|
|
577
|
+
}
|
|
578
|
+
return values.flatMap((value) => parseOwnerIds(value, root));
|
|
579
|
+
}
|
|
580
|
+
function localContextEntries(context) {
|
|
581
|
+
if (Array.isArray(context)) return context.flatMap(localContextEntries);
|
|
582
|
+
return isJsonMap(context) ? Object.entries(context) : [];
|
|
583
|
+
}
|
|
584
|
+
function parseOwnerIds(value, root) {
|
|
585
|
+
if (Array.isArray(value)) return value.flatMap((item) => parseOwnerIds(item, root));
|
|
586
|
+
if (isJsonMap(value)) {
|
|
587
|
+
const id = parseMapId(value, root);
|
|
588
|
+
return id == null ? [] : [id];
|
|
589
|
+
}
|
|
590
|
+
if (typeof value !== "string") return [];
|
|
591
|
+
try {
|
|
592
|
+
return [parseIri(value)];
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (error instanceof TypeError) return [];
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function parseMapId(map, root) {
|
|
599
|
+
let raw = map["@id"] ?? map.id;
|
|
600
|
+
for (const context of [map["@context"], root?.["@context"]]) for (const [alias, definition] of localContextEntries(context)) if ((typeof definition === "string" ? definition : isJsonMap(definition) ? definition["@id"] : null) === "@id" && typeof map[alias] === "string") {
|
|
601
|
+
raw = map[alias];
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
if (typeof raw !== "string") return null;
|
|
605
|
+
try {
|
|
606
|
+
return parseIri(raw);
|
|
607
|
+
} catch (error) {
|
|
608
|
+
if (error instanceof TypeError) return null;
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function visitJsonMaps(json, visit) {
|
|
613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
614
|
+
const pending = [{
|
|
615
|
+
value: json,
|
|
616
|
+
path: ""
|
|
617
|
+
}];
|
|
618
|
+
while (pending.length > 0) {
|
|
619
|
+
const { value, path } = pending.pop();
|
|
620
|
+
if (value == null || typeof value !== "object" || seen.has(value)) continue;
|
|
621
|
+
seen.add(value);
|
|
622
|
+
if (Array.isArray(value)) {
|
|
623
|
+
for (let i = value.length - 1; i >= 0; i--) pending.push({
|
|
624
|
+
value: value[i],
|
|
625
|
+
path: `${path}/${i}`
|
|
626
|
+
});
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const map = value;
|
|
630
|
+
const result = visit(map, path);
|
|
631
|
+
if (result !== void 0) return result;
|
|
632
|
+
for (const key of Object.keys(map).reverse()) {
|
|
633
|
+
if (key === "proof" || key === "@context") continue;
|
|
634
|
+
pending.push({
|
|
635
|
+
value: map[key],
|
|
636
|
+
path: `${path}/${key}`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
//#endregion
|
|
357
642
|
//#region src/federation/inbox.ts
|
|
358
643
|
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
359
644
|
const logger = getLogger([
|
|
@@ -512,8 +797,602 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
512
797
|
});
|
|
513
798
|
}
|
|
514
799
|
//#endregion
|
|
800
|
+
//#region src/federation/portable.ts
|
|
801
|
+
/**
|
|
802
|
+
* The media type that FEP-ef61 gateways must use for portable objects.
|
|
803
|
+
*/
|
|
804
|
+
const PORTABLE_OBJECT_CONTENT_TYPE = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
|
|
805
|
+
const GATEWAY_OBJECT_PATH_PATTERN = /^\/\.well-known\/apgateway\/did(?::|%3A)/i;
|
|
806
|
+
const BARE_DID_PATTERN = /^did:[a-z0-9]+:[^/?#]+$/i;
|
|
807
|
+
const DID_KEY_PREFIX = "did:key:";
|
|
808
|
+
const BASE58BTC_MULTIBASE_PATTERN = /^z[1-9A-HJ-NP-Za-km-z]+$/;
|
|
809
|
+
const GATEWAY_PATH_PREFIX = "/.well-known/apgateway/";
|
|
810
|
+
const HASHLINK_SCHEME_PATTERN = /^hl(?::|%3A)/i;
|
|
811
|
+
/**
|
|
812
|
+
* The route template of hashlink media requests, used for metrics and traces.
|
|
813
|
+
*/
|
|
814
|
+
const HASHLINK_MEDIA_ROUTE_TEMPLATE = "/.well-known/apgateway/hl:{digestMultibase}";
|
|
815
|
+
/**
|
|
816
|
+
* Recognizes an FEP-ef61 gateway request for a portable object, e.g.,
|
|
817
|
+
* `GET /.well-known/apgateway/did:key:z6Mk.../notes/123`.
|
|
818
|
+
*
|
|
819
|
+
* The query is not part of the portable ID, so it is ignored here. It is not
|
|
820
|
+
* removed from the request itself, which HTTP Signatures may cover.
|
|
821
|
+
*
|
|
822
|
+
* @param url The request URL.
|
|
823
|
+
* @returns The parsed request, or `null` if the URL is not a gateway request
|
|
824
|
+
* for a portable object, e.g., the gateway discovery endpoint or
|
|
825
|
+
* a hashlink media URL.
|
|
826
|
+
*/
|
|
827
|
+
function parsePortableGatewayRequest(url) {
|
|
828
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
829
|
+
if (!GATEWAY_OBJECT_PATH_PATTERN.test(url.pathname)) return null;
|
|
830
|
+
let id;
|
|
831
|
+
let canonicalId;
|
|
832
|
+
try {
|
|
833
|
+
id = fromCompatibleEf61Id(url.origin + url.pathname);
|
|
834
|
+
if (id == null) return null;
|
|
835
|
+
canonicalId = canonicalizePortableUri(formatIri(id));
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error instanceof TypeError) return {
|
|
838
|
+
type: "malformed",
|
|
839
|
+
error
|
|
840
|
+
};
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
const authority = getFe34Origin(id);
|
|
844
|
+
try {
|
|
845
|
+
assertBase58BtcDidKey(authority);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
if (error instanceof TypeError) return {
|
|
848
|
+
type: "malformed",
|
|
849
|
+
error
|
|
850
|
+
};
|
|
851
|
+
throw error;
|
|
852
|
+
}
|
|
853
|
+
const href = id.href;
|
|
854
|
+
return {
|
|
855
|
+
type: "object",
|
|
856
|
+
portableRequest: Object.freeze({
|
|
857
|
+
authority,
|
|
858
|
+
get id() {
|
|
859
|
+
return new URL(href);
|
|
860
|
+
}
|
|
861
|
+
}),
|
|
862
|
+
canonicalId,
|
|
863
|
+
path: id.pathname
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Recognizes an FEP-ef61 portable ID, i.e., an `ap:` or `ap+ef61:` URI, or
|
|
868
|
+
* a compatible identifier on any gateway, and splits it into its DID and
|
|
869
|
+
* the object path to route.
|
|
870
|
+
*
|
|
871
|
+
* It accepts the same IDs as the gateway endpoint does: the query, e.g.,
|
|
872
|
+
* location hints, and the fragment are ignored, and a malformed DID, or
|
|
873
|
+
* a `did:key` DID that is not encoded in base58-btc, is refused.
|
|
874
|
+
*
|
|
875
|
+
* @param uri The ID to recognize.
|
|
876
|
+
* @returns The DID normalized as an FEP-fe34 origin, and the object path,
|
|
877
|
+
* or `null` if the ID is not a portable ID or is malformed.
|
|
878
|
+
*/
|
|
879
|
+
function parsePortableId(uri) {
|
|
880
|
+
if (uri.username !== "" || uri.password !== "") return null;
|
|
881
|
+
if (uri.protocol !== "ap:" && uri.protocol !== "ap+ef61:") {
|
|
882
|
+
const request = parsePortableGatewayRequest(uri);
|
|
883
|
+
if (request?.type !== "object") return null;
|
|
884
|
+
return {
|
|
885
|
+
authority: request.portableRequest.authority,
|
|
886
|
+
path: request.path
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const id = parseIri(uri);
|
|
891
|
+
canonicalizePortableUri(formatIri(id));
|
|
892
|
+
const authority = getFe34Origin(id);
|
|
893
|
+
assertBase58BtcDidKey(authority);
|
|
894
|
+
return {
|
|
895
|
+
authority,
|
|
896
|
+
path: id.pathname
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (error instanceof TypeError) return null;
|
|
900
|
+
throw error;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Builds a portable ID from a DID authority and an object path.
|
|
905
|
+
* @param authority The bare DID, e.g., `did:key:z6Mk...`.
|
|
906
|
+
* @param path The object path, e.g., `/notes/123`.
|
|
907
|
+
* @returns The portable ID.
|
|
908
|
+
* @throws {TypeError} If the authority is not a bare DID, or it is
|
|
909
|
+
* a `did:key` DID that is not encoded in base58-btc.
|
|
910
|
+
*/
|
|
911
|
+
function buildPortableUri(authority, path) {
|
|
912
|
+
if (typeof authority !== "string" || !BARE_DID_PATTERN.test(authority)) throw new TypeError("The authority of a portable ID must be a DID without a path, query, or fragment.");
|
|
913
|
+
const did = getFe34Origin(authority);
|
|
914
|
+
assertBase58BtcDidKey(did);
|
|
915
|
+
return parseIri(`ap+ef61://${did}${path}`);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Checks that a normalized DID, if it is a `did:key` DID, is encoded in
|
|
919
|
+
* base58-btc, as FEP-ef61 requires so that the same key does not yield two
|
|
920
|
+
* portable IDs. This only checks the encoding, not whether the DID is
|
|
921
|
+
* a valid key.
|
|
922
|
+
* @param did The DID normalized by `getFe34Origin()`.
|
|
923
|
+
* @throws {TypeError} If the DID is a `did:key` DID that is not encoded in
|
|
924
|
+
* base58-btc.
|
|
925
|
+
*/
|
|
926
|
+
function assertBase58BtcDidKey(did) {
|
|
927
|
+
if (did.startsWith(DID_KEY_PREFIX) && !BASE58BTC_MULTIBASE_PATTERN.test(did.slice(8))) throw new TypeError("The did:key authority of a portable ID must be encoded in base58-btc, i.e., start with z.");
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Recognizes an FEP-ef61 gateway request for a resource addressed by
|
|
931
|
+
* a hashlink, e.g.,
|
|
932
|
+
* `GET /.well-known/apgateway/hl:zQmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n`.
|
|
933
|
+
*
|
|
934
|
+
* The rest of the path, including any slashes, is taken as the hashlink, as
|
|
935
|
+
* a digest in a multibase encoding like base64 may contain slashes. The query
|
|
936
|
+
* is not part of the hashlink, so it is ignored here.
|
|
937
|
+
*
|
|
938
|
+
* @param url The request URL.
|
|
939
|
+
* @returns The parsed request, or `null` if the URL is not a gateway request
|
|
940
|
+
* for a hashlink, e.g., a request for a portable object.
|
|
941
|
+
*/
|
|
942
|
+
function parseHashlinkGatewayRequest(url) {
|
|
943
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
944
|
+
if (!url.pathname.startsWith(GATEWAY_PATH_PREFIX)) return null;
|
|
945
|
+
const encoded = url.pathname.slice(23);
|
|
946
|
+
if (!HASHLINK_SCHEME_PATTERN.test(encoded)) return null;
|
|
947
|
+
let digestMultibase;
|
|
948
|
+
let digest;
|
|
949
|
+
let multihash;
|
|
950
|
+
try {
|
|
951
|
+
let hashlink;
|
|
952
|
+
try {
|
|
953
|
+
hashlink = decodeURIComponent(encoded);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
throw new TypeError("Invalid percent-encoding in the hashlink.", { cause: error });
|
|
956
|
+
}
|
|
957
|
+
({digestMultibase} = parseHashlink(hashlink));
|
|
958
|
+
({digest} = parseDigestMultibase(digestMultibase));
|
|
959
|
+
multihash = decodeMultibase(digestMultibase);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error instanceof TypeError) return {
|
|
962
|
+
type: "malformed",
|
|
963
|
+
error
|
|
964
|
+
};
|
|
965
|
+
throw error;
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
type: "media",
|
|
969
|
+
media: Object.freeze({
|
|
970
|
+
hashlink: `hl:${digestMultibase}`,
|
|
971
|
+
digestMultibase,
|
|
972
|
+
algorithm: "sha2-256",
|
|
973
|
+
digest,
|
|
974
|
+
multihash
|
|
975
|
+
})
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/federation/portable-inbox.ts
|
|
980
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
981
|
+
/**
|
|
982
|
+
* Fills in the defaults of {@link PortableInboxForwardingOptions} and
|
|
983
|
+
* validates them.
|
|
984
|
+
* @param options The options.
|
|
985
|
+
* @returns The resolved options.
|
|
986
|
+
* @throws {RangeError} If `maxTargets` is not a non-negative integer, if `ttl`
|
|
987
|
+
* is not positive, if `deadline` is negative or longer
|
|
988
|
+
* than 2,147,483,647 milliseconds (about 24.8 days), or
|
|
989
|
+
* if either duration has calendar units, i.e., weeks,
|
|
990
|
+
* months, or years, whose length depends on the date.
|
|
991
|
+
*/
|
|
992
|
+
function resolvePortableInboxForwardingOptions(options = {}) {
|
|
993
|
+
const maxTargets = options.maxTargets ?? 10;
|
|
994
|
+
if (!Number.isInteger(maxTargets) || maxTargets < 0) throw new RangeError("portableInboxForwarding.maxTargets must be a non-negative integer.");
|
|
995
|
+
const ttl = Temporal.Duration.from(options.ttl ?? { days: 30 });
|
|
996
|
+
if (ttl.total("millisecond") <= 0) throw new RangeError("portableInboxForwarding.ttl must be positive.");
|
|
997
|
+
const deadline = Temporal.Duration.from(options.deadline ?? { seconds: 10 });
|
|
998
|
+
const deadlineMs = deadline.total("millisecond");
|
|
999
|
+
if (deadlineMs < 0 || deadlineMs > MAX_TIMEOUT_MS) throw new RangeError(`portableInboxForwarding.deadline must be between 0 and ${MAX_TIMEOUT_MS} milliseconds.`);
|
|
1000
|
+
return {
|
|
1001
|
+
maxTargets,
|
|
1002
|
+
ttl,
|
|
1003
|
+
deadline
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Decides whether this server accepts a delivery to a portable inbox on behalf
|
|
1008
|
+
* of the given actor, which the actor dispatcher returned for the identifier
|
|
1009
|
+
* in the requested inbox path.
|
|
1010
|
+
*
|
|
1011
|
+
* The actor document comes from the application, so it is trusted as is and
|
|
1012
|
+
* its proof is not verified here.
|
|
1013
|
+
* @param actor The actor the actor dispatcher returned.
|
|
1014
|
+
* @param options The requested DID, the canonical form of the requested inbox
|
|
1015
|
+
* ID, and the origin of this server.
|
|
1016
|
+
* @returns The resolution.
|
|
1017
|
+
*/
|
|
1018
|
+
function resolvePortableInboxRecipient(actor, { authority, canonicalInboxId, localOrigin }) {
|
|
1019
|
+
if (actor == null) return {
|
|
1020
|
+
status: "rejected",
|
|
1021
|
+
reason: "notFound"
|
|
1022
|
+
};
|
|
1023
|
+
const id = actor.id;
|
|
1024
|
+
const did = id == null || !isPortableId(id) ? null : getPortableDid(id);
|
|
1025
|
+
if (id == null || did == null) return {
|
|
1026
|
+
status: "rejected",
|
|
1027
|
+
reason: "notPortable"
|
|
1028
|
+
};
|
|
1029
|
+
if (did !== authority) return {
|
|
1030
|
+
status: "rejected",
|
|
1031
|
+
reason: "authorityMismatch"
|
|
1032
|
+
};
|
|
1033
|
+
const inboxId = actor.inboxId == null ? null : toPortableUri(actor.inboxId);
|
|
1034
|
+
if (inboxId == null || getCanonicalPortableId(inboxId) !== canonicalInboxId) return {
|
|
1035
|
+
status: "rejected",
|
|
1036
|
+
reason: "inboxMismatch"
|
|
1037
|
+
};
|
|
1038
|
+
const gateways = actor.gateways;
|
|
1039
|
+
if (!gateways.some((g) => isGatewayUrl(g) && g.origin === localOrigin)) return {
|
|
1040
|
+
status: "rejected",
|
|
1041
|
+
reason: "notGateway"
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
status: "accepted",
|
|
1045
|
+
recipient: {
|
|
1046
|
+
actorId: id,
|
|
1047
|
+
inboxId,
|
|
1048
|
+
canonicalInboxId,
|
|
1049
|
+
gateways
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Gets the portable form of an `ap:` or `ap+ef61:` URI or a compatible
|
|
1055
|
+
* identifier, keeping its query.
|
|
1056
|
+
* @returns The portable URI, or `null` if the ID is neither, or is malformed.
|
|
1057
|
+
*/
|
|
1058
|
+
function toPortableUri(id) {
|
|
1059
|
+
if (isPortableUri(id)) return id;
|
|
1060
|
+
try {
|
|
1061
|
+
return fromCompatibleEf61Id(id);
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
if (error instanceof TypeError) return null;
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const RFC9421_DELIVERY_COMPONENTS = [
|
|
1068
|
+
"@method",
|
|
1069
|
+
"@target-uri",
|
|
1070
|
+
"content-digest"
|
|
1071
|
+
];
|
|
1072
|
+
const DRAFT_DELIVERY_COMPONENTS = [
|
|
1073
|
+
"(request-target)",
|
|
1074
|
+
"host",
|
|
1075
|
+
"digest"
|
|
1076
|
+
];
|
|
1077
|
+
/**
|
|
1078
|
+
* Checks whether an HTTP Signature covers the method, the target URI, and the
|
|
1079
|
+
* body of a request, which both Fedify and Mitra sign when forwarding.
|
|
1080
|
+
* @param signature The signature.
|
|
1081
|
+
* @returns `true` if the signature covers them.
|
|
1082
|
+
*/
|
|
1083
|
+
function coversDelivery(signature) {
|
|
1084
|
+
return (signature.label == null ? DRAFT_DELIVERY_COMPONENTS : RFC9421_DELIVERY_COMPONENTS).every((name) => signature.components.some((c) => c.value === name && Object.keys(c.params).length < 1));
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Forwards an activity received in a portable inbox to the inboxes of the same
|
|
1088
|
+
* portable actor on its other gateways, as FEP-ef61 recommends.
|
|
1089
|
+
*
|
|
1090
|
+
* FEP-ef61 forbids forwarding an activity from an inbox more than once, so
|
|
1091
|
+
* each target gateway is claimed with an atomic compare-and-swap before the
|
|
1092
|
+
* activity is handed off, and a claim is never released, even if the hand-off
|
|
1093
|
+
* fails, since a failed hand-off does not prove that nothing was delivered.
|
|
1094
|
+
* Forwarding is therefore best effort. Without {@link KvStore.cas}, nothing is
|
|
1095
|
+
* forwarded.
|
|
1096
|
+
*
|
|
1097
|
+
* Forwarded requests are signed with HTTP Signatures by the RSA key among
|
|
1098
|
+
* {@link ForwardPortableInboxActivityParameters.getKeys}, if any, so that
|
|
1099
|
+
* servers requiring HTTP Signatures accept them, and are sent unsigned
|
|
1100
|
+
* otherwise. Either way, the receiving gateways authenticate the activity by
|
|
1101
|
+
* its own proof.
|
|
1102
|
+
*
|
|
1103
|
+
* If the delivery is signed with a gateway key of the recipient by one of its
|
|
1104
|
+
* other gateways, that gateway has the activity already, so it is claimed
|
|
1105
|
+
* without forwarding the activity to it. See {@link identifySendingGateway}.
|
|
1106
|
+
* @param parameters The parameters.
|
|
1107
|
+
* @returns The target inbox URLs that the activity was handed off to.
|
|
1108
|
+
*/
|
|
1109
|
+
async function forwardPortableInboxActivity(parameters) {
|
|
1110
|
+
const logger = getLogger([
|
|
1111
|
+
"fedify",
|
|
1112
|
+
"federation",
|
|
1113
|
+
"inbox"
|
|
1114
|
+
]);
|
|
1115
|
+
const { recipient, activityId, excludedOrigins, kv, kvPrefix, outboxQueue } = parameters;
|
|
1116
|
+
const options = parameters.options ?? resolvePortableInboxForwardingOptions();
|
|
1117
|
+
const { maxTargets, ttl } = options;
|
|
1118
|
+
if (kv.cas == null || maxTargets < 1) return [];
|
|
1119
|
+
const deadline = Date.now() + options.deadline.total("millisecond");
|
|
1120
|
+
const canonicalActivityId = getCanonicalPortableId(activityId) ?? activityId.href;
|
|
1121
|
+
const claimKey = (gateway) => [
|
|
1122
|
+
...kvPrefix,
|
|
1123
|
+
recipient.canonicalInboxId,
|
|
1124
|
+
canonicalActivityId,
|
|
1125
|
+
gateway
|
|
1126
|
+
];
|
|
1127
|
+
let targets = getForwardingTargets(recipient, excludedOrigins);
|
|
1128
|
+
const sender = await identifySendingGateway(parameters, targets.map((t) => t.gateway), claimKey, deadline);
|
|
1129
|
+
if (sender != null) {
|
|
1130
|
+
logger.debug("Not forwarding activity {activityId} from the portable inbox {inbox} back to the gateway {gateway}, which delivered it.", {
|
|
1131
|
+
activityId: activityId.href,
|
|
1132
|
+
inbox: recipient.canonicalInboxId,
|
|
1133
|
+
gateway: sender
|
|
1134
|
+
});
|
|
1135
|
+
await kv.cas(claimKey(sender), void 0, true, { ttl });
|
|
1136
|
+
targets = targets.filter((t) => t.gateway !== sender);
|
|
1137
|
+
}
|
|
1138
|
+
if (targets.length > maxTargets) {
|
|
1139
|
+
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.", {
|
|
1140
|
+
inbox: recipient.canonicalInboxId,
|
|
1141
|
+
activityId: activityId.href,
|
|
1142
|
+
max: maxTargets
|
|
1143
|
+
});
|
|
1144
|
+
targets.length = maxTargets;
|
|
1145
|
+
}
|
|
1146
|
+
const claimed = [];
|
|
1147
|
+
for (const { gateway, inbox } of targets) if (await kv.cas(claimKey(gateway), void 0, true, { ttl })) claimed.push(inbox);
|
|
1148
|
+
if (claimed.length < 1) return [];
|
|
1149
|
+
logger.debug("Forwarding activity {activityId} from the portable inbox {inbox} to other gateways:\n{targets}", {
|
|
1150
|
+
activityId: activityId.href,
|
|
1151
|
+
inbox: recipient.canonicalInboxId,
|
|
1152
|
+
targets: claimed.map((t) => t.href)
|
|
1153
|
+
});
|
|
1154
|
+
if (outboxQueue == null) await forwardImmediately(parameters, claimed, deadline);
|
|
1155
|
+
else await enqueueForwarding(parameters, outboxQueue, claimed);
|
|
1156
|
+
return claimed;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Identifies the gateway of the recipient that forwarded a delivery to its
|
|
1160
|
+
* portable inbox, by the delivery's HTTP Signature made with the recipient's
|
|
1161
|
+
* gateway key, which FEP-ef61 has each gateway use for requests on behalf of
|
|
1162
|
+
* an actor. Since the activity is authenticated by its own proof, the
|
|
1163
|
+
* signature only tells which gateway to skip, and a delivery whose signature
|
|
1164
|
+
* is invalid or cannot be verified is still forwarded to every gateway.
|
|
1165
|
+
*
|
|
1166
|
+
* Verifying the signature fetches the recipient's actor document from the
|
|
1167
|
+
* sending gateway, as gateway keys are not cached, so only the first signature
|
|
1168
|
+
* that may save a request is verified: one that covers the delivery and names
|
|
1169
|
+
* a gateway key of the recipient on one of the forwarding targets that the
|
|
1170
|
+
* activity has not been forwarded to yet.
|
|
1171
|
+
* @param parameters The forwarding parameters.
|
|
1172
|
+
* @param targets The origins of the gateways to forward to.
|
|
1173
|
+
* @param claimKey Gets the key of the claim for a gateway.
|
|
1174
|
+
* @param deadline The time by which identification has to finish, in
|
|
1175
|
+
* milliseconds since the epoch; afterwards, the sending
|
|
1176
|
+
* gateway is left unidentified.
|
|
1177
|
+
* @returns The origin of the sending gateway, or `null` if it is not
|
|
1178
|
+
* identified.
|
|
1179
|
+
*/
|
|
1180
|
+
async function identifySendingGateway({ recipient, activityId, kv, signatures }, targets, claimKey, deadline) {
|
|
1181
|
+
if (signatures == null || signatures.length < 1) return null;
|
|
1182
|
+
const logger = getLogger([
|
|
1183
|
+
"fedify",
|
|
1184
|
+
"federation",
|
|
1185
|
+
"inbox"
|
|
1186
|
+
]);
|
|
1187
|
+
const actorId = getCanonicalPortableId(recipient.actorId);
|
|
1188
|
+
if (actorId == null) return null;
|
|
1189
|
+
const isGatewayKeyOfRecipient = (keyId) => {
|
|
1190
|
+
if (!isCompatibleKeyId(keyId) || !targets.includes(keyId.origin)) return false;
|
|
1191
|
+
const base = new URL(keyId.href);
|
|
1192
|
+
base.hash = "";
|
|
1193
|
+
return getCanonicalPortableId(base) === actorId;
|
|
1194
|
+
};
|
|
1195
|
+
let signature;
|
|
1196
|
+
for (const candidate of signatures) if (candidate.coversDelivery && isGatewayKeyOfRecipient(candidate.keyId) && await kv.get(claimKey(candidate.keyId.origin)) === void 0) {
|
|
1197
|
+
signature = candidate;
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
if (signature == null) return null;
|
|
1201
|
+
const { keyId } = signature;
|
|
1202
|
+
const logProperties = {
|
|
1203
|
+
activityId: activityId.href,
|
|
1204
|
+
inbox: recipient.canonicalInboxId,
|
|
1205
|
+
keyId: keyId.href
|
|
1206
|
+
};
|
|
1207
|
+
const verification = Promise.resolve().then(() => signature.verify()).then((key) => {
|
|
1208
|
+
if (key == null) logger.debug("The HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} is not verified with the key {keyId}; forwarding it without identifying the sending gateway.", logProperties);
|
|
1209
|
+
return key;
|
|
1210
|
+
}, (error) => {
|
|
1211
|
+
logger.debug("Failed to verify the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} with the key {keyId}; forwarding it without identifying the sending gateway:\n{error}", {
|
|
1212
|
+
...logProperties,
|
|
1213
|
+
error
|
|
1214
|
+
});
|
|
1215
|
+
return null;
|
|
1216
|
+
});
|
|
1217
|
+
let timer;
|
|
1218
|
+
const key = await Promise.race([verification, new Promise((resolve) => {
|
|
1219
|
+
timer = setTimeout(() => {
|
|
1220
|
+
logger.debug("Verifying the HTTP Signature of the delivery of activity {activityId} to the portable inbox {inbox} did not finish in time; forwarding it without identifying the sending gateway.", logProperties);
|
|
1221
|
+
resolve(null);
|
|
1222
|
+
}, Math.max(0, deadline - Date.now()));
|
|
1223
|
+
})]);
|
|
1224
|
+
clearTimeout(timer);
|
|
1225
|
+
if (key == null) return null;
|
|
1226
|
+
if (key.id == null || key.id.href !== keyId.href || !isGatewayKeyOfRecipient(key.id) || key.ownerId == null || !isPortableId(key.ownerId) || getCanonicalPortableId(key.ownerId) !== actorId) {
|
|
1227
|
+
logger.debug("The delivery of activity {activityId} to the portable inbox {inbox} is not signed with a gateway key of the recipient; forwarding it without identifying the sending gateway.", logProperties);
|
|
1228
|
+
return null;
|
|
1229
|
+
}
|
|
1230
|
+
return key.id.origin;
|
|
1231
|
+
}
|
|
1232
|
+
function getForwardingTargets(recipient, excludedOrigins) {
|
|
1233
|
+
const logger = getLogger([
|
|
1234
|
+
"fedify",
|
|
1235
|
+
"federation",
|
|
1236
|
+
"inbox"
|
|
1237
|
+
]);
|
|
1238
|
+
const seen = new Set(excludedOrigins);
|
|
1239
|
+
const targets = [];
|
|
1240
|
+
for (const gateway of recipient.gateways) {
|
|
1241
|
+
if (!isGatewayUrl(gateway) || seen.has(gateway.origin)) continue;
|
|
1242
|
+
seen.add(gateway.origin);
|
|
1243
|
+
let inbox;
|
|
1244
|
+
try {
|
|
1245
|
+
inbox = toCompatibleEf61Id(recipient.inboxId, gateway.origin);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
if (!(error instanceof TypeError)) throw error;
|
|
1248
|
+
logger.warn("Cannot forward to the gateway {gateway}, as the portable inbox {inbox} cannot be represented as its compatible identifier.", {
|
|
1249
|
+
gateway: gateway.origin,
|
|
1250
|
+
inbox: recipient.canonicalInboxId
|
|
1251
|
+
});
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
targets.push({
|
|
1255
|
+
gateway: gateway.origin,
|
|
1256
|
+
inbox
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
return targets;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Resolves the key that signs forwarded requests: the first RSASSA-PKCS1-v1_5
|
|
1263
|
+
* key, as {@link sendActivity} signs requests only with such a key. Since
|
|
1264
|
+
* the targets have already been claimed, the activity is forwarded unsigned
|
|
1265
|
+
* rather than not at all if the key cannot be resolved or used.
|
|
1266
|
+
*/
|
|
1267
|
+
async function resolveForwardingKey({ getKeys, activityId }) {
|
|
1268
|
+
if (getKeys == null) return null;
|
|
1269
|
+
const logger = getLogger([
|
|
1270
|
+
"fedify",
|
|
1271
|
+
"federation",
|
|
1272
|
+
"inbox"
|
|
1273
|
+
]);
|
|
1274
|
+
let key;
|
|
1275
|
+
try {
|
|
1276
|
+
key = (await getKeys()).find((k) => k.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5");
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
logger.error("Failed to get the gateway keys to sign forwarded requests with; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
1279
|
+
activityId: activityId.href,
|
|
1280
|
+
error
|
|
1281
|
+
});
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
if (key == null) return null;
|
|
1285
|
+
try {
|
|
1286
|
+
validateCryptoKey(key.privateKey, "private");
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
if (!(error instanceof TypeError)) throw error;
|
|
1289
|
+
logger.warn("The gateway key {keyId} cannot sign forwarded requests; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
1290
|
+
keyId: key.keyId.href,
|
|
1291
|
+
activityId: activityId.href,
|
|
1292
|
+
error
|
|
1293
|
+
});
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
return key;
|
|
1297
|
+
}
|
|
1298
|
+
async function forwardImmediately(parameters, inboxes, deadlineAt) {
|
|
1299
|
+
const { activity, activityId, activityType, allowPrivateAddress, specDeterminer, meterProvider, tracerProvider } = parameters;
|
|
1300
|
+
const logger = getLogger([
|
|
1301
|
+
"fedify",
|
|
1302
|
+
"federation",
|
|
1303
|
+
"inbox"
|
|
1304
|
+
]);
|
|
1305
|
+
const deadline = Math.max(0, deadlineAt - Date.now());
|
|
1306
|
+
const forwarding = resolveForwardingKey(parameters).then((key) => Promise.all(inboxes.map((inbox) => sendActivity({
|
|
1307
|
+
activity,
|
|
1308
|
+
activityId: activityId.href,
|
|
1309
|
+
activityType,
|
|
1310
|
+
keys: key == null ? [] : [key],
|
|
1311
|
+
inbox,
|
|
1312
|
+
allowPrivateAddress,
|
|
1313
|
+
specDeterminer,
|
|
1314
|
+
meterProvider,
|
|
1315
|
+
tracerProvider
|
|
1316
|
+
}).catch((error) => {
|
|
1317
|
+
logger.error("Failed to forward activity {activityId} to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
1318
|
+
activityId: activityId.href,
|
|
1319
|
+
inbox: inbox.href,
|
|
1320
|
+
error
|
|
1321
|
+
});
|
|
1322
|
+
}))));
|
|
1323
|
+
let timer;
|
|
1324
|
+
const timedOut = await Promise.race([forwarding.then(() => false), new Promise((resolve) => {
|
|
1325
|
+
timer = setTimeout(() => resolve(true), deadline);
|
|
1326
|
+
})]);
|
|
1327
|
+
clearTimeout(timer);
|
|
1328
|
+
if (timedOut) logger.warn("Forwarding activity {activityId} to other gateways did not finish in {deadline} ms; responding to the delivery without waiting for it.", {
|
|
1329
|
+
activityId: activityId.href,
|
|
1330
|
+
deadline
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
async function enqueueForwarding(parameters, outboxQueue, inboxes) {
|
|
1334
|
+
const { activity, activityId, activityType, baseUrl, startQueue, meterProvider } = parameters;
|
|
1335
|
+
const logger = getLogger([
|
|
1336
|
+
"fedify",
|
|
1337
|
+
"federation",
|
|
1338
|
+
"inbox"
|
|
1339
|
+
]);
|
|
1340
|
+
const key = await resolveForwardingKey(parameters);
|
|
1341
|
+
const keys = [];
|
|
1342
|
+
if (key != null) try {
|
|
1343
|
+
keys.push({
|
|
1344
|
+
keyId: key.keyId.href,
|
|
1345
|
+
privateKey: await exportJwk(key.privateKey)
|
|
1346
|
+
});
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
logger.error("Failed to export the gateway key {keyId}; forwarding activity {activityId} without HTTP Signatures:\n{error}", {
|
|
1349
|
+
keyId: key.keyId.href,
|
|
1350
|
+
activityId: activityId.href,
|
|
1351
|
+
error
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
startQueue?.();
|
|
1355
|
+
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
1356
|
+
const traceContext = {};
|
|
1357
|
+
propagation.inject(context.active(), traceContext);
|
|
1358
|
+
await Promise.all(inboxes.map(async (inbox) => {
|
|
1359
|
+
const message = {
|
|
1360
|
+
type: "outbox",
|
|
1361
|
+
id: crypto.randomUUID(),
|
|
1362
|
+
baseUrl,
|
|
1363
|
+
keys,
|
|
1364
|
+
activity,
|
|
1365
|
+
activityId: activityId.href,
|
|
1366
|
+
activityType,
|
|
1367
|
+
inbox: inbox.href,
|
|
1368
|
+
sharedInbox: false,
|
|
1369
|
+
started,
|
|
1370
|
+
attempt: 0,
|
|
1371
|
+
headers: {},
|
|
1372
|
+
traceContext
|
|
1373
|
+
};
|
|
1374
|
+
try {
|
|
1375
|
+
await outboxQueue.enqueue(message);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
logger.error("Failed to enqueue activity {activityId} to forward to {inbox}; it will not be forwarded to the inbox again:\n{error}", {
|
|
1378
|
+
activityId: activityId.href,
|
|
1379
|
+
inbox: inbox.href,
|
|
1380
|
+
error
|
|
1381
|
+
});
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
recordOutboxEnqueue(meterProvider, outboxQueue, message);
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
//#endregion
|
|
515
1388
|
//#region src/federation/handler.ts
|
|
516
1389
|
const rawInboxContextFactorySymbol = Symbol("fedify.rawInboxContextFactory");
|
|
1390
|
+
const INBOX_COMPOUND_PROOF_LIMITS = {
|
|
1391
|
+
maxDepth: 64,
|
|
1392
|
+
maxMaps: 1e4,
|
|
1393
|
+
maxProofs: 32,
|
|
1394
|
+
maxBytes: 10 * 1024 * 1024
|
|
1395
|
+
};
|
|
517
1396
|
function isRemoteContextLoadingFailure$1(error) {
|
|
518
1397
|
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
519
1398
|
}
|
|
@@ -529,7 +1408,7 @@ function isInvalidJsonLdError(error) {
|
|
|
529
1408
|
return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
530
1409
|
}
|
|
531
1410
|
function isValidationTypeError(error) {
|
|
532
|
-
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
1411
|
+
return error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:|Invalid FEP-ef61 gateway:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
533
1412
|
}
|
|
534
1413
|
function isPermanentActivityParseError(error) {
|
|
535
1414
|
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
@@ -596,12 +1475,221 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
596
1475
|
if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
|
|
597
1476
|
}
|
|
598
1477
|
const jsonLd = await object.toJsonLd(context);
|
|
1478
|
+
if (object instanceof Tombstone) return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
|
|
1479
|
+
status: 410,
|
|
1480
|
+
headers: {
|
|
1481
|
+
"Content-Type": "application/activity+json",
|
|
1482
|
+
Vary: "Accept"
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
await warnServedCompatibleObject(context, object, getCompactRootId(jsonLd));
|
|
599
1486
|
return new Response(JSON.stringify(jsonLd), { headers: {
|
|
600
1487
|
"Content-Type": "application/activity+json",
|
|
601
1488
|
Vary: "Accept"
|
|
602
1489
|
} });
|
|
603
1490
|
}
|
|
604
|
-
|
|
1491
|
+
/**
|
|
1492
|
+
* Handles an FEP-ef61 gateway request for a portable object through an object
|
|
1493
|
+
* dispatcher, or the actor dispatcher adapted to one. The object is served
|
|
1494
|
+
* only if its ID canonically matches the requested portable ID and it
|
|
1495
|
+
* satisfies the FEP-ef61 proof policy.
|
|
1496
|
+
*
|
|
1497
|
+
* FEP-ef61 forbids serving a non-public object to anyone but its intended
|
|
1498
|
+
* audience. If the dispatcher has an authorization predicate, it decides who
|
|
1499
|
+
* may retrieve the object, and the response is marked with
|
|
1500
|
+
* `Cache-Control: private`, as it depends on the requester. Otherwise, only
|
|
1501
|
+
* a publicly addressed object is served, and any other object is responded
|
|
1502
|
+
* to with `404 Not Found`, as if this server did not store it. Actors, and
|
|
1503
|
+
* anything the actor dispatcher returns, are exempt from the latter, as
|
|
1504
|
+
* actors have no audience.
|
|
1505
|
+
*
|
|
1506
|
+
* A {@link Tombstone} is served with `410 Gone` if it has an Object Integrity
|
|
1507
|
+
* Proof made with a key of the DID in its ID. An unsigned tombstone cannot
|
|
1508
|
+
* be served as a portable object, so the response is the same as for
|
|
1509
|
+
* an object that this server does not store, i.e., `404 Not Found`.
|
|
1510
|
+
* @template TContextData The context data to pass to the context.
|
|
1511
|
+
* @param request The HTTP request.
|
|
1512
|
+
* @param parameters The parameters for handling the portable object.
|
|
1513
|
+
* @returns A promise that resolves to an HTTP response.
|
|
1514
|
+
*/
|
|
1515
|
+
async function handlePortableObject(request, { values, context, objectDispatcher, authorizePredicate, canonicalId, kind, onNotFound, onUnauthorized }) {
|
|
1516
|
+
const logger = getLogger([
|
|
1517
|
+
"fedify",
|
|
1518
|
+
"federation",
|
|
1519
|
+
"object"
|
|
1520
|
+
]);
|
|
1521
|
+
if (objectDispatcher == null) return await onNotFound(request);
|
|
1522
|
+
const object = await objectDispatcher(context, values);
|
|
1523
|
+
if (object == null) return await onNotFound(request);
|
|
1524
|
+
if (!isRequestedPortableObject(object, canonicalId)) {
|
|
1525
|
+
logger.debug("The object {objectId} does not match the requested portable object {portableId}.", {
|
|
1526
|
+
objectId: object.id?.href,
|
|
1527
|
+
portableId: canonicalId
|
|
1528
|
+
});
|
|
1529
|
+
return await onNotFound(request);
|
|
1530
|
+
}
|
|
1531
|
+
if (authorizePredicate != null) {
|
|
1532
|
+
if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
|
|
1533
|
+
}
|
|
1534
|
+
const contextLoader = getNormalizationContextLoader(context.contextLoader);
|
|
1535
|
+
let jsonLd;
|
|
1536
|
+
let root;
|
|
1537
|
+
try {
|
|
1538
|
+
jsonLd = await object.toJsonLd(context);
|
|
1539
|
+
root = await expandJsonLdRoot(jsonLd, contextLoader);
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
logger.error("Failed to serialize the portable object {portableId}:\n{error}", {
|
|
1542
|
+
portableId: canonicalId,
|
|
1543
|
+
error
|
|
1544
|
+
});
|
|
1545
|
+
return portableObjectInternalServerError(request);
|
|
1546
|
+
}
|
|
1547
|
+
const servedId = root?.["@id"];
|
|
1548
|
+
if (root == null || typeof servedId !== "string" || !isRequestedPortableId(servedId, canonicalId)) {
|
|
1549
|
+
logger.debug("The serialized object does not match the requested portable object {portableId}.", { portableId: canonicalId });
|
|
1550
|
+
return await onNotFound(request);
|
|
1551
|
+
}
|
|
1552
|
+
if (authorizePredicate == null && kind === "object" && !isActor(object) && !isPubliclyAddressedNode(root)) {
|
|
1553
|
+
logger.debug("Not serving the portable object {portableId}, as it is not publicly addressed and its object dispatcher has no authorization predicate; responding as if this server did not store the object. Set an authorization predicate that checks if the request is signed by an actor in the object's audience to serve it.", { portableId: canonicalId });
|
|
1554
|
+
return await onNotFound(request);
|
|
1555
|
+
}
|
|
1556
|
+
let result;
|
|
1557
|
+
try {
|
|
1558
|
+
result = await verifyPortableObjectProof(jsonLd, {
|
|
1559
|
+
contextLoader,
|
|
1560
|
+
documentLoader: context.documentLoader,
|
|
1561
|
+
tracerProvider: context.tracerProvider,
|
|
1562
|
+
meterProvider: context.meterProvider
|
|
1563
|
+
});
|
|
1564
|
+
} catch (error) {
|
|
1565
|
+
logger.error("Failed to verify the Object Integrity Proofs of the portable object {portableId}:\n{error}", {
|
|
1566
|
+
portableId: canonicalId,
|
|
1567
|
+
error
|
|
1568
|
+
});
|
|
1569
|
+
return portableObjectInternalServerError(request);
|
|
1570
|
+
}
|
|
1571
|
+
const tombstone = object instanceof Tombstone;
|
|
1572
|
+
if (!result.verified) {
|
|
1573
|
+
if (tombstone && result.reason.type === "missingProof") {
|
|
1574
|
+
logger.debug("Not serving the tombstone of the portable object {portableId}, as it has no Object Integrity Proof; responding as if this server did not store the object.", { portableId: canonicalId });
|
|
1575
|
+
return await onNotFound(request);
|
|
1576
|
+
}
|
|
1577
|
+
if (tombstone || result.reason.type !== "unsecuredCollection" && result.reason.type !== "unsupportedObjectType") {
|
|
1578
|
+
if (result.reason.type === "invalidGateways") logger.error("Refusing to serve the portable actor {portableId}, as its gateways property is missing, empty, or has an item that is not an HTTP(S) origin. FEP-ef61 requires a portable actor to list at least one gateway where it can be retrieved.", {
|
|
1579
|
+
portableId: canonicalId,
|
|
1580
|
+
reason: result.reason.type
|
|
1581
|
+
});
|
|
1582
|
+
else logger.error("Refusing to serve the portable object {portableId}, as it does not satisfy the FEP-ef61 proof policy: {reason}. Portable actors, activities, objects, and tombstones need an Object Integrity Proof made with a key of the DID in their ID.", {
|
|
1583
|
+
portableId: canonicalId,
|
|
1584
|
+
reason: result.reason.type
|
|
1585
|
+
});
|
|
1586
|
+
return portableObjectInternalServerError(request);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (!tombstone) await warnServedCompatibleObject(context, object, servedId);
|
|
1590
|
+
const headers = new Headers({
|
|
1591
|
+
"Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
|
|
1592
|
+
Vary: "Accept"
|
|
1593
|
+
});
|
|
1594
|
+
if (authorizePredicate != null) headers.set("Cache-Control", "private");
|
|
1595
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), {
|
|
1596
|
+
status: tombstone ? 410 : 200,
|
|
1597
|
+
headers
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
async function warnServedCompatibleObject(context, object, serializedId) {
|
|
1601
|
+
try {
|
|
1602
|
+
if (isActor(object)) return;
|
|
1603
|
+
const id = typeof serializedId === "string" ? parseIri(serializedId) : serializedId ?? object.id;
|
|
1604
|
+
if (id == null || !isCompatibleEf61Iri(id)) return;
|
|
1605
|
+
const did = getPortableDid(id);
|
|
1606
|
+
if (did == null) return;
|
|
1607
|
+
const ownerIds = object instanceof Activity ? object.actorIds : object.attributionIds;
|
|
1608
|
+
for (const ownerId of ownerIds) {
|
|
1609
|
+
if (getPortableDid(ownerId) !== did) continue;
|
|
1610
|
+
const gateway = await getLocalFirstGateway(context, ownerId, did);
|
|
1611
|
+
if (gateway == null) continue;
|
|
1612
|
+
warnCompatibleId(id, did, gateway, object instanceof Activity ? "activity" : "object");
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
getLogger([
|
|
1617
|
+
"fedify",
|
|
1618
|
+
"federation",
|
|
1619
|
+
"object"
|
|
1620
|
+
]).debug("Could not check the first gateway of {objectId}: {error}", {
|
|
1621
|
+
objectId: object.id?.href,
|
|
1622
|
+
error
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function isRequestedPortableObject(object, canonicalId) {
|
|
1627
|
+
return object.id != null && getCanonicalPortableId(object.id) === canonicalId;
|
|
1628
|
+
}
|
|
1629
|
+
async function expandJsonLdRoot(jsonLd, contextLoader) {
|
|
1630
|
+
const expanded = await jsonld.expand(jsonLd, {
|
|
1631
|
+
documentLoader: contextLoader,
|
|
1632
|
+
keepFreeFloatingNodes: true
|
|
1633
|
+
});
|
|
1634
|
+
if (expanded.length !== 1) return null;
|
|
1635
|
+
return expanded[0];
|
|
1636
|
+
}
|
|
1637
|
+
function isRequestedPortableId(id, canonicalId) {
|
|
1638
|
+
if (!/^ap(?:\+ef61)?:\/\//i.test(id)) return URL.canParse(id) && getCanonicalPortableId(new URL(id)) === canonicalId;
|
|
1639
|
+
try {
|
|
1640
|
+
return canonicalizePortableUri(id) === canonicalId;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (error instanceof TypeError) return false;
|
|
1643
|
+
throw error;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function portableObjectInternalServerError(request) {
|
|
1647
|
+
return new Response(request.method === "HEAD" ? null : "Internal server error.", {
|
|
1648
|
+
status: 500,
|
|
1649
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Checks the portable objects embedded in a portable collection or its page
|
|
1654
|
+
* that is about to be served against the FEP-ef61 proof policy. See
|
|
1655
|
+
* `verifyServedPortableObjects()`.
|
|
1656
|
+
* @returns `true` if the collection can be served.
|
|
1657
|
+
*/
|
|
1658
|
+
async function verifyPortableCollection(jsonLd, context, portable) {
|
|
1659
|
+
const logger = getLogger([
|
|
1660
|
+
"fedify",
|
|
1661
|
+
"federation",
|
|
1662
|
+
"collection"
|
|
1663
|
+
]);
|
|
1664
|
+
try {
|
|
1665
|
+
const result = await verifyServedPortableObjects(jsonLd, {
|
|
1666
|
+
contextLoader: context.contextLoader,
|
|
1667
|
+
documentLoader: context.documentLoader,
|
|
1668
|
+
tracerProvider: context.tracerProvider,
|
|
1669
|
+
meterProvider: context.meterProvider
|
|
1670
|
+
});
|
|
1671
|
+
if (result.verified) return true;
|
|
1672
|
+
logger.error("Refusing to serve the portable collection {collectionId}, as the portable object {objectId} it embeds at {path} does not satisfy the FEP-ef61 proof policy: {reason}. Embedded portable actors, activities, and objects need an Object Integrity Proof made with a key of the DID in their ID, and their own @context.", {
|
|
1673
|
+
collectionId: portable.id.href,
|
|
1674
|
+
objectId: result.id,
|
|
1675
|
+
path: result.path,
|
|
1676
|
+
reason: result.reason
|
|
1677
|
+
});
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
logger.error("Failed to verify the portable objects embedded in the portable collection {collectionId}:\n{error}", {
|
|
1680
|
+
collectionId: portable.id.href,
|
|
1681
|
+
error
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
function respondWithPortableCollection(request, jsonLd) {
|
|
1687
|
+
return new Response(request.method === "HEAD" ? null : JSON.stringify(jsonLd), { headers: {
|
|
1688
|
+
"Content-Type": PORTABLE_OBJECT_CONTENT_TYPE,
|
|
1689
|
+
Vary: "Accept"
|
|
1690
|
+
} });
|
|
1691
|
+
}
|
|
1692
|
+
const BUILT_IN_COLLECTION_METRIC_KINDS = /* @__PURE__ */ new Set([
|
|
605
1693
|
"inbox",
|
|
606
1694
|
"outbox",
|
|
607
1695
|
"following",
|
|
@@ -638,13 +1726,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
638
1726
|
* @param parameters The parameters for handling the collection.
|
|
639
1727
|
* @returns A promise that resolves to an HTTP response.
|
|
640
1728
|
*/
|
|
641
|
-
async function handleCollection(request, { name: name$
|
|
642
|
-
const spanName = name$
|
|
1729
|
+
async function handleCollection(request, { name: name$2, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, portable, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
|
|
1730
|
+
const spanName = name$2.trim().replace(/\s+/g, "_");
|
|
643
1731
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
644
1732
|
const tracer = tracerProvider.getTracer(name, version);
|
|
645
1733
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
646
1734
|
const metricBase = {
|
|
647
|
-
kind: getCollectionMetricKind(name$
|
|
1735
|
+
kind: getCollectionMetricKind(name$2),
|
|
648
1736
|
page: cursor != null,
|
|
649
1737
|
dispatcher: "built_in"
|
|
650
1738
|
};
|
|
@@ -662,8 +1750,13 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
662
1750
|
};
|
|
663
1751
|
try {
|
|
664
1752
|
if (collectionCallbacks == null) return finish(await onNotFound(request), "not_found");
|
|
1753
|
+
if (portable != null && collectionCallbacks.authorizePredicate != null) {
|
|
1754
|
+
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
|
|
1755
|
+
}
|
|
665
1756
|
let collection;
|
|
666
|
-
const baseUri = uriGetter(identifier);
|
|
1757
|
+
const baseUri = portable?.id ?? uriGetter(identifier);
|
|
1758
|
+
const pageBase = portable?.view ?? context.url;
|
|
1759
|
+
const attribution = portable?.attribution ?? null;
|
|
667
1760
|
if (cursor == null) {
|
|
668
1761
|
const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
|
|
669
1762
|
const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
|
|
@@ -685,7 +1778,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
685
1778
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
686
1779
|
return await onNotFound(request);
|
|
687
1780
|
}
|
|
688
|
-
const items = filterCollectionItems(page.items, name$
|
|
1781
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
689
1782
|
itemCount = items.length;
|
|
690
1783
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
691
1784
|
return items;
|
|
@@ -703,29 +1796,31 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
703
1796
|
if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
|
|
704
1797
|
collection = new OrderedCollection({
|
|
705
1798
|
id: baseUri,
|
|
706
|
-
|
|
1799
|
+
attribution,
|
|
1800
|
+
totalItems: totalItemCount ?? (portable == null ? null : itemsOrResponse.length),
|
|
707
1801
|
items: itemsOrResponse
|
|
708
1802
|
});
|
|
709
1803
|
} else {
|
|
710
1804
|
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
711
|
-
const first = new URL(
|
|
1805
|
+
const first = new URL(pageBase);
|
|
712
1806
|
first.searchParams.set("cursor", firstCursor);
|
|
713
1807
|
let last = null;
|
|
714
1808
|
if (lastCursor != null) {
|
|
715
|
-
last = new URL(
|
|
1809
|
+
last = new URL(pageBase);
|
|
716
1810
|
last.searchParams.set("cursor", lastCursor);
|
|
717
1811
|
}
|
|
718
1812
|
collection = new OrderedCollection({
|
|
719
1813
|
id: baseUri,
|
|
1814
|
+
attribution,
|
|
720
1815
|
totalItems: totalItemCount ?? null,
|
|
721
1816
|
first,
|
|
722
1817
|
last
|
|
723
1818
|
});
|
|
724
1819
|
}
|
|
725
1820
|
} else {
|
|
726
|
-
const uri = new URL(baseUri);
|
|
1821
|
+
const uri = new URL(portable == null ? baseUri : pageBase);
|
|
727
1822
|
uri.searchParams.set("cursor", cursor);
|
|
728
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
1823
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
|
|
729
1824
|
kind: SpanKind.SERVER,
|
|
730
1825
|
attributes: {
|
|
731
1826
|
"activitypub.collection.id": uri.href,
|
|
@@ -741,7 +1836,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
741
1836
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
742
1837
|
return await onNotFound(request);
|
|
743
1838
|
}
|
|
744
|
-
const items = filterCollectionItems(page.items, name$
|
|
1839
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
745
1840
|
itemCount = items.length;
|
|
746
1841
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
747
1842
|
return {
|
|
@@ -763,28 +1858,30 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
763
1858
|
const { items, prevCursor, nextCursor } = pageOrResponse;
|
|
764
1859
|
let prev = null;
|
|
765
1860
|
if (prevCursor != null) {
|
|
766
|
-
prev = new URL(
|
|
1861
|
+
prev = new URL(pageBase);
|
|
767
1862
|
prev.searchParams.set("cursor", prevCursor);
|
|
768
1863
|
}
|
|
769
1864
|
let next = null;
|
|
770
1865
|
if (nextCursor != null) {
|
|
771
|
-
next = new URL(
|
|
1866
|
+
next = new URL(pageBase);
|
|
772
1867
|
next.searchParams.set("cursor", nextCursor);
|
|
773
1868
|
}
|
|
774
|
-
const partOf = new URL(
|
|
1869
|
+
const partOf = new URL(pageBase);
|
|
775
1870
|
partOf.searchParams.delete("cursor");
|
|
776
1871
|
collection = new OrderedCollectionPage({
|
|
777
1872
|
id: uri,
|
|
1873
|
+
attribution,
|
|
778
1874
|
prev,
|
|
779
1875
|
next,
|
|
780
1876
|
items,
|
|
781
1877
|
partOf
|
|
782
1878
|
});
|
|
783
1879
|
}
|
|
784
|
-
if (collectionCallbacks.authorizePredicate != null) {
|
|
1880
|
+
if (portable == null && collectionCallbacks.authorizePredicate != null) {
|
|
785
1881
|
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
|
|
786
1882
|
}
|
|
787
1883
|
const jsonLd = await collection.toJsonLd(context);
|
|
1884
|
+
if (portable != null) return await verifyPortableCollection(jsonLd, context, portable) ? finish(respondWithPortableCollection(request, jsonLd), "served") : finish(portableObjectInternalServerError(request), "error");
|
|
788
1885
|
return finish(new Response(JSON.stringify(jsonLd), { headers: {
|
|
789
1886
|
"Content-Type": "application/activity+json",
|
|
790
1887
|
Vary: "Accept"
|
|
@@ -951,7 +2048,7 @@ async function handleOutbox(request, { identifier, context: ctx, outboxContextFa
|
|
|
951
2048
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
952
2049
|
});
|
|
953
2050
|
}
|
|
954
|
-
if (!activity.actorIds.every((actorId) => actorId
|
|
2051
|
+
if (!activity.actorIds.every((actorId) => isSameObjectId(actorId, expectedActorId))) {
|
|
955
2052
|
const error = /* @__PURE__ */ new Error("The activity actor does not match the outbox owner.");
|
|
956
2053
|
logger.error("The posted activity actor does not match outbox owner {identifier}.", {
|
|
957
2054
|
identifier,
|
|
@@ -1220,6 +2317,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
|
|
|
1220
2317
|
* @returns A promise that resolves to an HTTP response.
|
|
1221
2318
|
*/
|
|
1222
2319
|
async function handleInbox(request, options) {
|
|
2320
|
+
if (options.observation == null) {
|
|
2321
|
+
const observation = new InboxObservation({
|
|
2322
|
+
kind: options.portableInbox != null ? "portable" : options.recipient == null ? "shared" : "personal",
|
|
2323
|
+
recipient: options.recipient
|
|
2324
|
+
});
|
|
2325
|
+
return await observation.runAndFinish(() => options.context, options.inboxRequestFinishedHandler, () => handleInbox(request, {
|
|
2326
|
+
...options,
|
|
2327
|
+
observation
|
|
2328
|
+
}));
|
|
2329
|
+
}
|
|
1223
2330
|
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.inbox", {
|
|
1224
2331
|
kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
|
|
1225
2332
|
attributes: { "activitypub.shared_inbox": options.recipient == null }
|
|
@@ -1228,12 +2335,14 @@ async function handleInbox(request, options) {
|
|
|
1228
2335
|
try {
|
|
1229
2336
|
return await handleInboxInternal(request, options, span);
|
|
1230
2337
|
} catch (e) {
|
|
2338
|
+
options.observation.hasException = true;
|
|
1231
2339
|
span.setStatus({
|
|
1232
2340
|
code: SpanStatusCode.ERROR,
|
|
1233
2341
|
message: String(e)
|
|
1234
2342
|
});
|
|
1235
2343
|
throw e;
|
|
1236
2344
|
} finally {
|
|
2345
|
+
options.observation.project(span);
|
|
1237
2346
|
span.end();
|
|
1238
2347
|
}
|
|
1239
2348
|
});
|
|
@@ -1247,7 +2356,9 @@ async function handleInbox(request, options) {
|
|
|
1247
2356
|
* @returns A promise that resolves to an HTTP response.
|
|
1248
2357
|
*/
|
|
1249
2358
|
async function handleInboxInternal(request, parameters, span) {
|
|
1250
|
-
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider } = parameters;
|
|
2359
|
+
const { recipient, context: ctx, inboxContextFactory, kv, kvPrefixes, publicKeyTtl, queue, actorDispatcher, inboxListeners, inboxErrorHandler, unverifiedActivityHandler, onNotFound, signatureTimeWindow, maxHttpSignatures, skipSignatureVerification, inboxChallengePolicy, meterProvider, tracerProvider, portableInbox } = parameters;
|
|
2360
|
+
const observation = parameters.observation;
|
|
2361
|
+
const signatureObservation = { [verificationObservation]: observation.verification };
|
|
1251
2362
|
const logger = getLogger([
|
|
1252
2363
|
"fedify",
|
|
1253
2364
|
"federation",
|
|
@@ -1260,7 +2371,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1260
2371
|
message: "Actor dispatcher is not set."
|
|
1261
2372
|
});
|
|
1262
2373
|
return await onNotFound(request);
|
|
1263
|
-
} else if (recipient != null) {
|
|
2374
|
+
} else if (recipient != null && portableInbox == null) {
|
|
1264
2375
|
const actor = await actorDispatcher(ctx, recipient);
|
|
1265
2376
|
if (actor == null || actor instanceof Tombstone) {
|
|
1266
2377
|
logger.error("Actor {recipient} not found.", { recipient });
|
|
@@ -1272,6 +2383,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1272
2383
|
}
|
|
1273
2384
|
}
|
|
1274
2385
|
if (request.bodyUsed) {
|
|
2386
|
+
observation.result = {
|
|
2387
|
+
disposition: "failed",
|
|
2388
|
+
reason: "bodyUnavailable"
|
|
2389
|
+
};
|
|
1275
2390
|
logger.error("Request body has already been read.", { recipient });
|
|
1276
2391
|
span.setStatus({
|
|
1277
2392
|
code: SpanStatusCode.ERROR,
|
|
@@ -1282,6 +2397,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1282
2397
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1283
2398
|
});
|
|
1284
2399
|
} else if (request.body?.locked) {
|
|
2400
|
+
observation.result = {
|
|
2401
|
+
disposition: "failed",
|
|
2402
|
+
reason: "bodyUnavailable"
|
|
2403
|
+
};
|
|
1285
2404
|
logger.error("Request body is locked.", { recipient });
|
|
1286
2405
|
span.setStatus({
|
|
1287
2406
|
code: SpanStatusCode.ERROR,
|
|
@@ -1292,10 +2411,27 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1292
2411
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1293
2412
|
});
|
|
1294
2413
|
}
|
|
2414
|
+
observation.stage = "parse";
|
|
2415
|
+
observation.result = {
|
|
2416
|
+
disposition: "rejected",
|
|
2417
|
+
reason: "invalidJson"
|
|
2418
|
+
};
|
|
1295
2419
|
let json;
|
|
1296
2420
|
try {
|
|
1297
|
-
json = await request.clone().
|
|
2421
|
+
json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
|
|
1298
2422
|
} catch (error) {
|
|
2423
|
+
if (error instanceof BodyTooLargeError) {
|
|
2424
|
+
observation.result = {
|
|
2425
|
+
disposition: "rejected",
|
|
2426
|
+
reason: "bodyTooLarge"
|
|
2427
|
+
};
|
|
2428
|
+
request.body?.cancel(error).catch(() => {});
|
|
2429
|
+
span.setStatus({
|
|
2430
|
+
code: SpanStatusCode.ERROR,
|
|
2431
|
+
message: String(error)
|
|
2432
|
+
});
|
|
2433
|
+
return new Response("Inbox body too large.", { status: 413 });
|
|
2434
|
+
}
|
|
1299
2435
|
logger.error("Failed to parse JSON:\n{error}", {
|
|
1300
2436
|
recipient,
|
|
1301
2437
|
error
|
|
@@ -1318,7 +2454,21 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1318
2454
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1319
2455
|
});
|
|
1320
2456
|
}
|
|
1321
|
-
|
|
2457
|
+
observation.payload = {
|
|
2458
|
+
status: "parsed",
|
|
2459
|
+
value: structuredClone(json)
|
|
2460
|
+
};
|
|
2461
|
+
observation.result = {
|
|
2462
|
+
disposition: "rejected",
|
|
2463
|
+
reason: "invalidActivity"
|
|
2464
|
+
};
|
|
2465
|
+
observation.stage = "verify";
|
|
2466
|
+
const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, {
|
|
2467
|
+
documentLoader: ctx.documentLoader,
|
|
2468
|
+
contextLoader: ctx.contextLoader,
|
|
2469
|
+
tracerProvider,
|
|
2470
|
+
keyTtl: publicKeyTtl
|
|
2471
|
+
});
|
|
1322
2472
|
const jsonWithoutSig = detachSignature(json);
|
|
1323
2473
|
const hasLdSignature = hasSignature(json);
|
|
1324
2474
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
@@ -1342,6 +2492,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1342
2492
|
code: SpanStatusCode.ERROR,
|
|
1343
2493
|
message: `Failed to parse activity:\n${error}`
|
|
1344
2494
|
});
|
|
2495
|
+
observation.result = {
|
|
2496
|
+
disposition: "rejected",
|
|
2497
|
+
reason: "invalidActivity"
|
|
2498
|
+
};
|
|
1345
2499
|
return new Response("Invalid activity.", {
|
|
1346
2500
|
status: 400,
|
|
1347
2501
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1359,11 +2513,25 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1359
2513
|
recipient,
|
|
1360
2514
|
error
|
|
1361
2515
|
});
|
|
2516
|
+
observation.result = {
|
|
2517
|
+
disposition: "rejected",
|
|
2518
|
+
reason: "invalidJsonLd"
|
|
2519
|
+
};
|
|
1362
2520
|
return new Response("Invalid JSON-LD.", {
|
|
1363
2521
|
status: 400,
|
|
1364
2522
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1365
2523
|
});
|
|
1366
2524
|
}
|
|
2525
|
+
observation.verification.attempts.push({
|
|
2526
|
+
mechanism: "linkedData",
|
|
2527
|
+
subject: {
|
|
2528
|
+
id: null,
|
|
2529
|
+
pointer: ""
|
|
2530
|
+
},
|
|
2531
|
+
checks: [],
|
|
2532
|
+
status: "error",
|
|
2533
|
+
error
|
|
2534
|
+
});
|
|
1367
2535
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
1368
2536
|
if (!skipSignatureVerification) deferredLdSignatureError = error;
|
|
1369
2537
|
logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
|
|
@@ -1375,6 +2543,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1375
2543
|
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
1376
2544
|
try {
|
|
1377
2545
|
ldSigVerified = await verifyCompactJsonLd(compactedJson, {
|
|
2546
|
+
...signatureObservation,
|
|
1378
2547
|
contextLoader: ctx.contextLoader,
|
|
1379
2548
|
documentLoader: ctx.documentLoader,
|
|
1380
2549
|
keyCache,
|
|
@@ -1388,6 +2557,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1388
2557
|
recipient,
|
|
1389
2558
|
error
|
|
1390
2559
|
});
|
|
2560
|
+
observation.result = {
|
|
2561
|
+
disposition: "rejected",
|
|
2562
|
+
reason: "invalidJsonLd"
|
|
2563
|
+
};
|
|
1391
2564
|
return new Response("Invalid JSON-LD.", {
|
|
1392
2565
|
status: 400,
|
|
1393
2566
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1407,6 +2580,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1407
2580
|
recipient,
|
|
1408
2581
|
error: parseError
|
|
1409
2582
|
});
|
|
2583
|
+
observation.result = {
|
|
2584
|
+
disposition: "rejected",
|
|
2585
|
+
reason: "invalidJsonLd"
|
|
2586
|
+
};
|
|
1410
2587
|
return new Response("Invalid JSON-LD.", {
|
|
1411
2588
|
status: 400,
|
|
1412
2589
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1420,13 +2597,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1420
2597
|
}
|
|
1421
2598
|
let activity = null;
|
|
1422
2599
|
let activityVerified = false;
|
|
2600
|
+
let proofVerified = false;
|
|
1423
2601
|
if (ldSigVerified) {
|
|
1424
2602
|
logger.debug("Linked Data Signatures are verified.", {
|
|
1425
2603
|
recipient,
|
|
1426
2604
|
json
|
|
1427
2605
|
});
|
|
1428
2606
|
try {
|
|
1429
|
-
activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
2607
|
+
activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
1430
2608
|
...ctx,
|
|
1431
2609
|
contextLoader: getNormalizationContextLoader(ctx.contextLoader)
|
|
1432
2610
|
});
|
|
@@ -1436,6 +2614,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1436
2614
|
return await respondInvalidActivity(error);
|
|
1437
2615
|
}
|
|
1438
2616
|
activityVerified = true;
|
|
2617
|
+
if (!skipSignatureVerification && isPortableActivity(activity)) try {
|
|
2618
|
+
proofVerified = await verifyObject(Activity, jsonWithoutSig, {
|
|
2619
|
+
...signatureObservation,
|
|
2620
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
2621
|
+
documentLoader: ctx.documentLoader,
|
|
2622
|
+
keyCache,
|
|
2623
|
+
meterProvider,
|
|
2624
|
+
tracerProvider
|
|
2625
|
+
}) != null;
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
2628
|
+
logger.debug("Failed to verify the Object Integrity Proofs of the portable actor's activity:\n{error}", {
|
|
2629
|
+
recipient,
|
|
2630
|
+
error
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
1439
2633
|
} else {
|
|
1440
2634
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
1441
2635
|
recipient,
|
|
@@ -1443,11 +2637,13 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1443
2637
|
});
|
|
1444
2638
|
try {
|
|
1445
2639
|
activity = await verifyObject(Activity, jsonWithoutSig, {
|
|
2640
|
+
...signatureObservation,
|
|
1446
2641
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
1447
2642
|
documentLoader: ctx.documentLoader,
|
|
1448
2643
|
keyCache,
|
|
1449
2644
|
meterProvider,
|
|
1450
|
-
tracerProvider
|
|
2645
|
+
tracerProvider,
|
|
2646
|
+
verifyPortableObject: ctx.verifyPortableObject
|
|
1451
2647
|
});
|
|
1452
2648
|
} catch (error) {
|
|
1453
2649
|
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
@@ -1477,6 +2673,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1477
2673
|
code: SpanStatusCode.ERROR,
|
|
1478
2674
|
message: `Failed to parse activity:\n${error}`
|
|
1479
2675
|
});
|
|
2676
|
+
observation.result = {
|
|
2677
|
+
disposition: "rejected",
|
|
2678
|
+
reason: "invalidActivity"
|
|
2679
|
+
};
|
|
1480
2680
|
return new Response("Invalid activity.", {
|
|
1481
2681
|
status: 400,
|
|
1482
2682
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1492,6 +2692,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1492
2692
|
activity: json
|
|
1493
2693
|
});
|
|
1494
2694
|
activityVerified = true;
|
|
2695
|
+
proofVerified = true;
|
|
1495
2696
|
}
|
|
1496
2697
|
}
|
|
1497
2698
|
let httpSigKey = null;
|
|
@@ -1499,15 +2700,25 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1499
2700
|
if (activity == null) {
|
|
1500
2701
|
if (!skipSignatureVerification) {
|
|
1501
2702
|
const verification = await verifyRequestDetailed(request, {
|
|
2703
|
+
...signatureObservation,
|
|
1502
2704
|
contextLoader: ctx.contextLoader,
|
|
1503
2705
|
documentLoader: ctx.documentLoader,
|
|
1504
2706
|
timeWindow: signatureTimeWindow,
|
|
2707
|
+
maxSignatures: maxHttpSignatures,
|
|
1505
2708
|
keyCache,
|
|
1506
2709
|
meterProvider,
|
|
1507
2710
|
tracerProvider
|
|
1508
2711
|
});
|
|
1509
2712
|
if (verification.verified === false) {
|
|
1510
2713
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2714
|
+
observation.authentication = {
|
|
2715
|
+
status: "rejected",
|
|
2716
|
+
reason: { type: "verificationFailed" }
|
|
2717
|
+
};
|
|
2718
|
+
observation.result = {
|
|
2719
|
+
disposition: "rejected",
|
|
2720
|
+
reason: "authentication"
|
|
2721
|
+
};
|
|
1511
2722
|
const reason = verification.reason;
|
|
1512
2723
|
const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
|
|
1513
2724
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
|
|
@@ -1522,7 +2733,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1522
2733
|
});
|
|
1523
2734
|
if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1524
2735
|
try {
|
|
1525
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
|
2736
|
+
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
|
1526
2737
|
} catch (error) {
|
|
1527
2738
|
logger.error("Failed to parse activity:\n{error}", {
|
|
1528
2739
|
recipient,
|
|
@@ -1538,6 +2749,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1538
2749
|
recipient
|
|
1539
2750
|
});
|
|
1540
2751
|
}
|
|
2752
|
+
observation.result = {
|
|
2753
|
+
disposition: "rejected",
|
|
2754
|
+
reason: "invalidActivity"
|
|
2755
|
+
};
|
|
1541
2756
|
return new Response("Invalid activity.", {
|
|
1542
2757
|
status: 400,
|
|
1543
2758
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1560,6 +2775,11 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1560
2775
|
try {
|
|
1561
2776
|
response = await unverifiedActivityHandler(ctx, activity, reason);
|
|
1562
2777
|
} catch (error) {
|
|
2778
|
+
observation.result = {
|
|
2779
|
+
disposition: "failed",
|
|
2780
|
+
reason: "listenerError",
|
|
2781
|
+
error
|
|
2782
|
+
};
|
|
1563
2783
|
logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
|
|
1564
2784
|
error,
|
|
1565
2785
|
activity: json,
|
|
@@ -1576,7 +2796,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1576
2796
|
}
|
|
1577
2797
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1578
2798
|
}
|
|
1579
|
-
if (response instanceof Response)
|
|
2799
|
+
if (response instanceof Response) {
|
|
2800
|
+
observation.result = { disposition: "customResponse" };
|
|
2801
|
+
return response;
|
|
2802
|
+
}
|
|
1580
2803
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1581
2804
|
} else {
|
|
1582
2805
|
if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
|
|
@@ -1586,7 +2809,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1586
2809
|
httpSigKey = verification.key;
|
|
1587
2810
|
}
|
|
1588
2811
|
try {
|
|
1589
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
2812
|
+
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
1590
2813
|
...ctx,
|
|
1591
2814
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
|
|
1592
2815
|
});
|
|
@@ -1595,6 +2818,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1595
2818
|
return await respondInvalidActivity(error);
|
|
1596
2819
|
}
|
|
1597
2820
|
}
|
|
2821
|
+
observation.activity = activity;
|
|
2822
|
+
observation.stage = "policy";
|
|
2823
|
+
observation.result = {
|
|
2824
|
+
disposition: "rejected",
|
|
2825
|
+
reason: "authentication"
|
|
2826
|
+
};
|
|
1598
2827
|
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
1599
2828
|
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
|
1600
2829
|
span.addEvent("activitypub.activity.received", {
|
|
@@ -1604,8 +2833,83 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1604
2833
|
"http_signatures.verified": httpSigKey != null,
|
|
1605
2834
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
1606
2835
|
});
|
|
2836
|
+
if (!skipSignatureVerification && !proofVerified && (isPortableActivity(activity) || isPortableActorKey(httpSigKey))) {
|
|
2837
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2838
|
+
observation.authentication = {
|
|
2839
|
+
status: "rejected",
|
|
2840
|
+
reason: {
|
|
2841
|
+
type: "proofPolicy",
|
|
2842
|
+
policy: "portableActor"
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
logger.error("The activity {activityId} of the portable actor {actorId} is not authenticated by a valid Object Integrity Proof.", {
|
|
2846
|
+
activity: json,
|
|
2847
|
+
recipient,
|
|
2848
|
+
activityId: activity.id?.href,
|
|
2849
|
+
actorId: activity.actorId?.href,
|
|
2850
|
+
keyId: httpSigKey?.id?.href
|
|
2851
|
+
});
|
|
2852
|
+
span.setStatus({
|
|
2853
|
+
code: SpanStatusCode.ERROR,
|
|
2854
|
+
message: `The activity of the portable actor (${activity.actorId?.href}) is not authenticated by a valid Object Integrity Proof.`
|
|
2855
|
+
});
|
|
2856
|
+
return new Response("Activities of portable actors must have valid Object Integrity Proofs.", {
|
|
2857
|
+
status: 401,
|
|
2858
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
if (!skipSignatureVerification && activity.id != null && isPortableId(activity.id)) {
|
|
2862
|
+
const rejection = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
|
|
2863
|
+
const rejection = await verifyPortableActivityId(activity.id, jsonWithoutSig, {
|
|
2864
|
+
[verificationObservation]: verification,
|
|
2865
|
+
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
2866
|
+
documentLoader: ctx.documentLoader,
|
|
2867
|
+
keyCache,
|
|
2868
|
+
meterProvider,
|
|
2869
|
+
tracerProvider
|
|
2870
|
+
});
|
|
2871
|
+
if (rejection != null) verification.attempt.reason = {
|
|
2872
|
+
type: "proofPolicy",
|
|
2873
|
+
reason: rejection
|
|
2874
|
+
};
|
|
2875
|
+
return rejection;
|
|
2876
|
+
}, (value) => value == null);
|
|
2877
|
+
if (rejection != null) {
|
|
2878
|
+
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2879
|
+
observation.authentication = {
|
|
2880
|
+
status: "rejected",
|
|
2881
|
+
reason: {
|
|
2882
|
+
type: "proofPolicy",
|
|
2883
|
+
policy: "portableActivity",
|
|
2884
|
+
detail: rejection
|
|
2885
|
+
}
|
|
2886
|
+
};
|
|
2887
|
+
logger.error("The portable activity {activityId} is not authenticated by an Object Integrity Proof of its own DID: {reason}", {
|
|
2888
|
+
activity: json,
|
|
2889
|
+
recipient,
|
|
2890
|
+
activityId: activity.id.href,
|
|
2891
|
+
reason: rejection.type
|
|
2892
|
+
});
|
|
2893
|
+
span.setStatus({
|
|
2894
|
+
code: SpanStatusCode.ERROR,
|
|
2895
|
+
message: `The portable activity (${activity.id.href}) is not authenticated by an Object Integrity Proof of its own DID.`
|
|
2896
|
+
});
|
|
2897
|
+
return new Response("Portable activities must have valid Object Integrity Proofs made by their own DIDs.", {
|
|
2898
|
+
status: 401,
|
|
2899
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
1607
2903
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
1608
2904
|
if (deferredLdSignatureError != null) throw deferredLdSignatureError;
|
|
2905
|
+
observation.authentication = {
|
|
2906
|
+
status: "rejected",
|
|
2907
|
+
reason: {
|
|
2908
|
+
type: "actorKeyMismatch",
|
|
2909
|
+
key: snapshotKey(httpSigKey),
|
|
2910
|
+
actorIds: activity.actorIds.map((id) => new URL(id.href))
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
1609
2913
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1610
2914
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
|
1611
2915
|
activity: json,
|
|
@@ -1622,13 +2926,70 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1622
2926
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1623
2927
|
});
|
|
1624
2928
|
}
|
|
2929
|
+
if (!skipSignatureVerification) {
|
|
2930
|
+
const compoundJson = Array.isArray(json) ? json : jsonWithoutSig;
|
|
2931
|
+
if (inspectCompoundPortableObjectApplicability(compoundJson, INBOX_COMPOUND_PROOF_LIMITS) !== "absent") {
|
|
2932
|
+
const compoundProof = await observeAttempt(signatureObservation, "objectIntegrity", async (verification) => {
|
|
2933
|
+
const result = await verifyCompoundPortableObjectProofs(compoundJson, INBOX_COMPOUND_PROOF_LIMITS, {
|
|
2934
|
+
[verificationObservation]: verification,
|
|
2935
|
+
documentLoader: ctx.documentLoader,
|
|
2936
|
+
keyCache,
|
|
2937
|
+
meterProvider,
|
|
2938
|
+
tracerProvider
|
|
2939
|
+
});
|
|
2940
|
+
if (result.status === "unsupported") verification.attempt.reason = {
|
|
2941
|
+
type: "proofPolicy",
|
|
2942
|
+
reason: result.reason
|
|
2943
|
+
};
|
|
2944
|
+
else if (!result.verified) {
|
|
2945
|
+
const failed = result.portableObjects.find((object) => !object.verified);
|
|
2946
|
+
verification.attempt.reason = {
|
|
2947
|
+
type: "proofPolicy",
|
|
2948
|
+
reason: failed?.verified === false ? failed.reason : { type: "invalidProof" }
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
return result;
|
|
2952
|
+
}, (result) => result.status === "ok" && result.verified);
|
|
2953
|
+
if (compoundProof.status !== "ok" || !compoundProof.verified) {
|
|
2954
|
+
observation.authentication = {
|
|
2955
|
+
status: "rejected",
|
|
2956
|
+
reason: {
|
|
2957
|
+
type: "proofPolicy",
|
|
2958
|
+
policy: "compound"
|
|
2959
|
+
}
|
|
2960
|
+
};
|
|
2961
|
+
logger.error("Failed to verify compound portable Object Integrity Proofs.", {
|
|
2962
|
+
recipient,
|
|
2963
|
+
activity: json,
|
|
2964
|
+
reason: compoundProof.status === "unsupported" ? compoundProof.reason.type : "invalidProof"
|
|
2965
|
+
});
|
|
2966
|
+
span.setStatus({
|
|
2967
|
+
code: SpanStatusCode.ERROR,
|
|
2968
|
+
message: "Failed to verify compound portable Object Integrity Proofs."
|
|
2969
|
+
});
|
|
2970
|
+
return new Response("Failed to verify compound portable Object Integrity Proofs.", {
|
|
2971
|
+
status: 401,
|
|
2972
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
1625
2977
|
if (pendingNonceLabel != null) {
|
|
1626
2978
|
if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
|
|
2979
|
+
observation.authentication = {
|
|
2980
|
+
status: "rejected",
|
|
2981
|
+
reason: { type: "invalidNonce" }
|
|
2982
|
+
};
|
|
1627
2983
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1628
2984
|
logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
|
|
1629
2985
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1630
2986
|
}
|
|
1631
2987
|
}
|
|
2988
|
+
observation.authentication = skipSignatureVerification ? { status: "skipped" } : {
|
|
2989
|
+
status: "verified",
|
|
2990
|
+
attempts: observation.verification.attempts.filter((attempt) => attempt.status === "verified")
|
|
2991
|
+
};
|
|
2992
|
+
observation.stage = "dispatch";
|
|
1632
2993
|
const routeResult = await routeActivity({
|
|
1633
2994
|
context: ctx,
|
|
1634
2995
|
json,
|
|
@@ -1640,7 +3001,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1640
3001
|
inboxListeners,
|
|
1641
3002
|
inboxContextFactory,
|
|
1642
3003
|
listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
|
|
1643
|
-
inboxErrorHandler,
|
|
3004
|
+
inboxErrorHandler: async (context, error) => {
|
|
3005
|
+
observation.result = {
|
|
3006
|
+
disposition: "failed",
|
|
3007
|
+
reason: "listenerError",
|
|
3008
|
+
error
|
|
3009
|
+
};
|
|
3010
|
+
await inboxErrorHandler?.(context, error);
|
|
3011
|
+
},
|
|
1644
3012
|
kv,
|
|
1645
3013
|
kvPrefixes,
|
|
1646
3014
|
queue,
|
|
@@ -1649,31 +3017,78 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1649
3017
|
tracerProvider,
|
|
1650
3018
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
1651
3019
|
});
|
|
1652
|
-
if (
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
});
|
|
1656
|
-
else if (routeResult === "missingActor") return new Response("Missing actor.", {
|
|
1657
|
-
status: 400,
|
|
1658
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1659
|
-
});
|
|
1660
|
-
else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
|
|
1661
|
-
status: 202,
|
|
1662
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1663
|
-
});
|
|
1664
|
-
else if (routeResult === "unsupportedActivity") return new Response("", {
|
|
1665
|
-
status: 202,
|
|
1666
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3020
|
+
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.", {
|
|
3021
|
+
activityId: activity.id.href,
|
|
3022
|
+
inbox: portableInbox.recipient.canonicalInboxId
|
|
1667
3023
|
});
|
|
1668
|
-
else
|
|
3024
|
+
else try {
|
|
3025
|
+
await portableInbox.forward(json, activity.id, getTypeId(activity).href, listRequestSignatures(request, maxHttpSignatures).map((signature) => ({
|
|
3026
|
+
keyId: signature.keyId,
|
|
3027
|
+
coversDelivery: coversDelivery(signature),
|
|
3028
|
+
verify: async () => {
|
|
3029
|
+
const verification = await verifyRequestDetailed(selectRequestSignature(request, signature), {
|
|
3030
|
+
contextLoader: ctx.contextLoader,
|
|
3031
|
+
documentLoader: ctx.documentLoader,
|
|
3032
|
+
timeWindow: signatureTimeWindow,
|
|
3033
|
+
keyCache,
|
|
3034
|
+
meterProvider,
|
|
3035
|
+
tracerProvider
|
|
3036
|
+
});
|
|
3037
|
+
return verification.verified ? verification.key : null;
|
|
3038
|
+
}
|
|
3039
|
+
})));
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
logger.error("Failed to forward activity {activityId} to the other gateways of the portable inbox {inbox}:\n{error}", {
|
|
3042
|
+
activityId: activity.id.href,
|
|
3043
|
+
inbox: portableInbox.recipient.canonicalInboxId,
|
|
3044
|
+
error
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
if (routeResult === "alreadyProcessed") {
|
|
3048
|
+
observation.result = { disposition: "duplicate" };
|
|
3049
|
+
return new Response(`Activity <${activity.id}> has already been processed.`, {
|
|
3050
|
+
status: 202,
|
|
3051
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3052
|
+
});
|
|
3053
|
+
} else if (routeResult === "missingActor") {
|
|
3054
|
+
observation.result = {
|
|
3055
|
+
disposition: "rejected",
|
|
3056
|
+
reason: "missingActor"
|
|
3057
|
+
};
|
|
3058
|
+
return new Response("Missing actor.", {
|
|
3059
|
+
status: 400,
|
|
3060
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3061
|
+
});
|
|
3062
|
+
} else if (routeResult === "enqueued") {
|
|
3063
|
+
observation.result = { disposition: "enqueued" };
|
|
3064
|
+
return new Response("Activity is enqueued.", {
|
|
3065
|
+
status: 202,
|
|
3066
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3067
|
+
});
|
|
3068
|
+
} else if (routeResult === "unsupportedActivity") {
|
|
3069
|
+
observation.result = { disposition: "unhandled" };
|
|
3070
|
+
return new Response("", {
|
|
3071
|
+
status: 202,
|
|
3072
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3073
|
+
});
|
|
3074
|
+
} else if (routeResult === "error") return new Response("Internal server error.", {
|
|
1669
3075
|
status: 500,
|
|
1670
3076
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1671
3077
|
});
|
|
1672
|
-
else
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
3078
|
+
else {
|
|
3079
|
+
observation.result = { disposition: "processed" };
|
|
3080
|
+
return new Response("", {
|
|
3081
|
+
status: 202,
|
|
3082
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
1676
3085
|
}
|
|
3086
|
+
const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
|
|
3087
|
+
"success",
|
|
3088
|
+
"enqueued",
|
|
3089
|
+
"alreadyProcessed",
|
|
3090
|
+
"unsupportedActivity"
|
|
3091
|
+
]);
|
|
1677
3092
|
/**
|
|
1678
3093
|
* Handles a custom collection request.
|
|
1679
3094
|
* @template TItem The type of items in the collection.
|
|
@@ -1698,19 +3113,11 @@ function recordDeferredPendingCollectionMetrics(error, result, response) {
|
|
|
1698
3113
|
pendingCollectionMetricRecorders.delete(error);
|
|
1699
3114
|
recorder?.(result, response);
|
|
1700
3115
|
}
|
|
1701
|
-
async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
3116
|
+
async function _handleCustomCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate, portable }) {
|
|
1702
3117
|
verifyDefined(callbacks);
|
|
1703
3118
|
await authIfNeeded(context, values, callbacks);
|
|
1704
3119
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1705
|
-
|
|
1706
|
-
try {
|
|
1707
|
-
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
1708
|
-
handler.recordPendingCollectionMetrics("served", response);
|
|
1709
|
-
return response;
|
|
1710
|
-
} catch (e) {
|
|
1711
|
-
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
1712
|
-
throw e;
|
|
1713
|
-
}
|
|
3120
|
+
return await respondWithCustomCollection(request, new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate, portable).fetchCollection(cursor), context, portable);
|
|
1714
3121
|
}
|
|
1715
3122
|
/**
|
|
1716
3123
|
* Handles an ordered collection request.
|
|
@@ -1724,13 +3131,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
|
|
|
1724
3131
|
* @since 1.8.0
|
|
1725
3132
|
*/
|
|
1726
3133
|
const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
|
|
1727
|
-
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate }) {
|
|
3134
|
+
async function _handleOrderedCollection(request, { name, values, context, tracerProvider, meterProvider, collectionCallbacks: callbacks, filterPredicate, portable }) {
|
|
1728
3135
|
verifyDefined(callbacks);
|
|
1729
3136
|
await authIfNeeded(context, values, callbacks);
|
|
1730
3137
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1731
|
-
|
|
3138
|
+
return await respondWithCustomCollection(request, new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate, portable).fetchCollection(cursor), context, portable);
|
|
3139
|
+
}
|
|
3140
|
+
async function respondWithCustomCollection(request, handler, context, portable) {
|
|
1732
3141
|
try {
|
|
1733
|
-
const
|
|
3142
|
+
const jsonLd = await handler.toJsonLd();
|
|
3143
|
+
let response;
|
|
3144
|
+
if (portable == null) response = respondAsActivity(jsonLd);
|
|
3145
|
+
else if (await verifyPortableCollection(jsonLd, context, portable)) response = respondWithPortableCollection(request, jsonLd);
|
|
3146
|
+
else throw new PortableCollectionRefusedError();
|
|
1734
3147
|
handler.recordPendingCollectionMetrics("served", response);
|
|
1735
3148
|
return response;
|
|
1736
3149
|
} catch (e) {
|
|
@@ -1760,6 +3173,7 @@ var CustomCollectionHandler = class {
|
|
|
1760
3173
|
Collection;
|
|
1761
3174
|
CollectionPage;
|
|
1762
3175
|
filterPredicate;
|
|
3176
|
+
portable;
|
|
1763
3177
|
/**
|
|
1764
3178
|
* The tracer for telemetry.
|
|
1765
3179
|
* @type {Tracer}
|
|
@@ -1796,8 +3210,8 @@ var CustomCollectionHandler = class {
|
|
|
1796
3210
|
* @param CollectionPage The CollectionPage constructor.
|
|
1797
3211
|
* @param filterPredicate Optional filter predicate for items.
|
|
1798
3212
|
*/
|
|
1799
|
-
constructor(name$
|
|
1800
|
-
this.name = name$
|
|
3213
|
+
constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
|
|
3214
|
+
this.name = name$1;
|
|
1801
3215
|
this.values = values;
|
|
1802
3216
|
this.context = context;
|
|
1803
3217
|
this.callbacks = callbacks;
|
|
@@ -1806,9 +3220,14 @@ var CustomCollectionHandler = class {
|
|
|
1806
3220
|
this.Collection = Collection;
|
|
1807
3221
|
this.CollectionPage = CollectionPage;
|
|
1808
3222
|
this.filterPredicate = filterPredicate;
|
|
3223
|
+
this.portable = portable;
|
|
1809
3224
|
this.name = this.name.trim().replace(/\s+/g, "_");
|
|
1810
3225
|
this.#tracer = this.tracerProvider.getTracer(name, version);
|
|
1811
|
-
this.#id = new URL(this.context.url);
|
|
3226
|
+
if (portable == null) this.#id = new URL(this.context.url);
|
|
3227
|
+
else {
|
|
3228
|
+
const cursor = this.context.url.searchParams.get("cursor");
|
|
3229
|
+
this.#id = cursor == null ? new URL(portable.view) : appendCursorIfExists(portable.view, cursor);
|
|
3230
|
+
}
|
|
1812
3231
|
this.#dispatcher = callbacks.dispatcher.bind(callbacks);
|
|
1813
3232
|
}
|
|
1814
3233
|
/**
|
|
@@ -1859,6 +3278,7 @@ var CustomCollectionHandler = class {
|
|
|
1859
3278
|
this.recordPendingCollectionItemCount(true, items.length);
|
|
1860
3279
|
return {
|
|
1861
3280
|
id,
|
|
3281
|
+
attribution: this.portable?.attribution ?? null,
|
|
1862
3282
|
partOf,
|
|
1863
3283
|
items,
|
|
1864
3284
|
prev: this.appendToUrl(prevCursor),
|
|
@@ -1880,6 +3300,7 @@ var CustomCollectionHandler = class {
|
|
|
1880
3300
|
});
|
|
1881
3301
|
return {
|
|
1882
3302
|
id: this.#id,
|
|
3303
|
+
attribution: this.portable?.attribution ?? null,
|
|
1883
3304
|
first: this.appendToUrl(firstCursor),
|
|
1884
3305
|
last: this.appendToUrl(lastCursor),
|
|
1885
3306
|
totalItems
|
|
@@ -1896,6 +3317,7 @@ var CustomCollectionHandler = class {
|
|
|
1896
3317
|
this.recordPendingCollectionItemCount(false, items.length);
|
|
1897
3318
|
return {
|
|
1898
3319
|
id: this.#id,
|
|
3320
|
+
attribution: this.portable?.attribution ?? null,
|
|
1899
3321
|
totalItems,
|
|
1900
3322
|
items
|
|
1901
3323
|
};
|
|
@@ -2013,7 +3435,7 @@ var CustomCollectionHandler = class {
|
|
|
2013
3435
|
* @returns The URL with cursor appended, or null if cursor is null/undefined.
|
|
2014
3436
|
*/
|
|
2015
3437
|
appendToUrl(cursor) {
|
|
2016
|
-
return appendCursorIfExists(this.context.url, cursor);
|
|
3438
|
+
return appendCursorIfExists(this.portable?.view ?? this.context.url, cursor);
|
|
2017
3439
|
}
|
|
2018
3440
|
/**
|
|
2019
3441
|
* Gets the stored collection or collection page.
|
|
@@ -2090,6 +3512,12 @@ function exceptWrapper(handler) {
|
|
|
2090
3512
|
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
|
|
2091
3513
|
return response;
|
|
2092
3514
|
}
|
|
3515
|
+
case PortableCollectionRefusedError: {
|
|
3516
|
+
const response = portableObjectInternalServerError(request);
|
|
3517
|
+
recordDeferredPendingCollectionMetrics(error, "error", response);
|
|
3518
|
+
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "error", response));
|
|
3519
|
+
return response;
|
|
3520
|
+
}
|
|
2093
3521
|
case UnauthorizedError: {
|
|
2094
3522
|
const response = await onUnauthorized(request);
|
|
2095
3523
|
recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
|
|
@@ -2186,6 +3614,16 @@ var UnauthorizedError = class extends HandlerError {
|
|
|
2186
3614
|
}
|
|
2187
3615
|
};
|
|
2188
3616
|
/**
|
|
3617
|
+
* Error thrown when a portable collection is not served because it embeds
|
|
3618
|
+
* portable objects that do not satisfy the FEP-ef61 proof policy.
|
|
3619
|
+
* @since 2.4.0
|
|
3620
|
+
*/
|
|
3621
|
+
var PortableCollectionRefusedError = class extends HandlerError {
|
|
3622
|
+
constructor() {
|
|
3623
|
+
super("The portable collection embeds unverifiable portable objects.");
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
/**
|
|
2189
3627
|
* Responds with the given object in JSON-LD format.
|
|
2190
3628
|
*
|
|
2191
3629
|
* @param object The object to respond with.
|
|
@@ -2212,7 +3650,7 @@ async function respondWithObjectIfAcceptable(object, request, options) {
|
|
|
2212
3650
|
return response;
|
|
2213
3651
|
}
|
|
2214
3652
|
function generateNonce() {
|
|
2215
|
-
const bytes = new Uint8Array(16);
|
|
3653
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
2216
3654
|
crypto.getRandomValues(bytes);
|
|
2217
3655
|
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2218
3656
|
}
|
|
@@ -2265,22 +3703,310 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
|
|
|
2265
3703
|
parameters
|
|
2266
3704
|
}]);
|
|
2267
3705
|
}
|
|
2268
|
-
async function setKey(kv, key, policy) {
|
|
2269
|
-
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
2270
|
-
const ttl = Temporal.Duration.from({ seconds });
|
|
2271
|
-
await kv.set(key, true, { ttl });
|
|
3706
|
+
async function setKey(kv, key, policy) {
|
|
3707
|
+
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
3708
|
+
const ttl = Temporal.Duration.from({ seconds });
|
|
3709
|
+
await kv.set(key, true, { ttl });
|
|
3710
|
+
}
|
|
3711
|
+
const DEF_COMPONENTS = [
|
|
3712
|
+
"@method",
|
|
3713
|
+
"@target-uri",
|
|
3714
|
+
"@authority",
|
|
3715
|
+
"content-digest"
|
|
3716
|
+
];
|
|
3717
|
+
const MIN_COMPONENTS = [
|
|
3718
|
+
"@method",
|
|
3719
|
+
"@target-uri",
|
|
3720
|
+
"@authority"
|
|
3721
|
+
];
|
|
3722
|
+
/**
|
|
3723
|
+
* Checks whether an HTTP Signature key is a key of an FEP-ef61 portable
|
|
3724
|
+
* actor, i.e., its ID is a compatible identifier or an `ap:` or `ap+ef61:`
|
|
3725
|
+
* URI, and it is owned by a portable actor, which only the portable actor's
|
|
3726
|
+
* signed document can establish.
|
|
3727
|
+
*/
|
|
3728
|
+
function isPortableActorKey(key) {
|
|
3729
|
+
return key?.id != null && isPortableKeyId(key.id) && key.ownerId != null && isPortableId(key.ownerId);
|
|
3730
|
+
}
|
|
3731
|
+
/**
|
|
3732
|
+
* Checks whether an activity needs an Object Integrity Proof by its DID:
|
|
3733
|
+
* either it is performed by an [FEP-ef61] portable actor, or its own ID is
|
|
3734
|
+
* portable. Both `ap:` URIs and compatible identifiers count as portable.
|
|
3735
|
+
*
|
|
3736
|
+
* [FEP-ef61]: https://w3id.org/fep/ef61
|
|
3737
|
+
*/
|
|
3738
|
+
function isPortableActivity(activity) {
|
|
3739
|
+
return hasPortableActor(activity) || activity.id != null && isPortableId(activity.id);
|
|
3740
|
+
}
|
|
3741
|
+
/**
|
|
3742
|
+
* Verifies that a portable activity's own ID belongs to the DID that signed
|
|
3743
|
+
* it, on the JSON-LD document received. The ID the proof policy checks has
|
|
3744
|
+
* to be the parsed activity's ID, so that a remote context that changes
|
|
3745
|
+
* between loads cannot make the two name different objects.
|
|
3746
|
+
* @returns `null` if the ID is verified, or the reason why it is not.
|
|
3747
|
+
*/
|
|
3748
|
+
async function verifyPortableActivityId(activityId, jsonLd, options) {
|
|
3749
|
+
const expectedId = getCanonicalPortableId(activityId);
|
|
3750
|
+
if (expectedId == null) return { type: "invalidObjectId" };
|
|
3751
|
+
let verification;
|
|
3752
|
+
try {
|
|
3753
|
+
verification = await verifyPortableObjectProofWithRoot(jsonLd, options);
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
if (error instanceof InvalidPortableObjectIdError) return { type: "invalidObjectId" };
|
|
3756
|
+
if (!isPermanentActivityParseError(error)) throw error;
|
|
3757
|
+
return { type: "invalidDocument" };
|
|
3758
|
+
}
|
|
3759
|
+
const { result, root } = verification;
|
|
3760
|
+
if (!result.verified) return result.reason;
|
|
3761
|
+
const rootId = root?.["@id"];
|
|
3762
|
+
if (typeof rootId !== "string" || getCanonicalPortableId(parseIri(rootId)) !== expectedId) return { type: "subjectMismatch" };
|
|
3763
|
+
return null;
|
|
3764
|
+
}
|
|
3765
|
+
//#endregion
|
|
3766
|
+
//#region src/federation/portable-collection.ts
|
|
3767
|
+
/**
|
|
3768
|
+
* The query parameters of a gateway request for a portable collection that
|
|
3769
|
+
* do not select a view of the collection: the page cursor, and FEP-ef61
|
|
3770
|
+
* location hints (`@gateway`, and the legacy `gateways`).
|
|
3771
|
+
*/
|
|
3772
|
+
const NON_VIEW_PARAMETERS = /* @__PURE__ */ new Set([
|
|
3773
|
+
"cursor",
|
|
3774
|
+
"@gateway",
|
|
3775
|
+
"gateways"
|
|
3776
|
+
]);
|
|
3777
|
+
/**
|
|
3778
|
+
* Checks that a collection requested through the FEP-ef61 gateway endpoint
|
|
3779
|
+
* belongs to a portable actor under the requested DID, and determines
|
|
3780
|
+
* the collection's ID from the actor.
|
|
3781
|
+
*
|
|
3782
|
+
* Collection dispatchers return items, not documents with IDs, so unlike
|
|
3783
|
+
* object dispatchers, there is nothing else to tell whether the requested
|
|
3784
|
+
* DID controls the collection.
|
|
3785
|
+
*
|
|
3786
|
+
* @param actor The actor that owns the collection according to
|
|
3787
|
+
* the application, dispatched without the portable request,
|
|
3788
|
+
* or `null` if there is none.
|
|
3789
|
+
* @param options The requested DID, and either the requested collection's
|
|
3790
|
+
* canonical ID and the actor's property that has to refer to
|
|
3791
|
+
* it (for the built-in collections of an actor), or
|
|
3792
|
+
* the requested collection's portable ID (for a custom
|
|
3793
|
+
* collection, whose ID has the same form as the actor's).
|
|
3794
|
+
* @returns The resolution.
|
|
3795
|
+
*/
|
|
3796
|
+
function resolvePortableCollectionOwner(actor, options) {
|
|
3797
|
+
if (!isActor(actor)) return {
|
|
3798
|
+
status: "rejected",
|
|
3799
|
+
reason: "notFound"
|
|
3800
|
+
};
|
|
3801
|
+
const ownerId = actor.id;
|
|
3802
|
+
const did = ownerId == null || !isPortableId(ownerId) ? null : getPortableDid(ownerId);
|
|
3803
|
+
if (ownerId == null || did == null) return {
|
|
3804
|
+
status: "rejected",
|
|
3805
|
+
reason: "notPortable"
|
|
3806
|
+
};
|
|
3807
|
+
if (did !== options.authority) return {
|
|
3808
|
+
status: "rejected",
|
|
3809
|
+
reason: "authorityMismatch"
|
|
3810
|
+
};
|
|
3811
|
+
if ("portableId" in options) return {
|
|
3812
|
+
status: "accepted",
|
|
3813
|
+
ownerId,
|
|
3814
|
+
collectionId: getCustomCollectionId(ownerId, actor, options.portableId)
|
|
3815
|
+
};
|
|
3816
|
+
const property = options.property(actor);
|
|
3817
|
+
if (property == null || getCanonicalPortableId(property) !== options.canonicalId) return {
|
|
3818
|
+
status: "rejected",
|
|
3819
|
+
reason: "collectionMismatch"
|
|
3820
|
+
};
|
|
3821
|
+
const collectionId = new URL(property);
|
|
3822
|
+
collectionId.search = "";
|
|
3823
|
+
collectionId.hash = "";
|
|
3824
|
+
return {
|
|
3825
|
+
status: "accepted",
|
|
3826
|
+
ownerId,
|
|
3827
|
+
collectionId
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3831
|
+
* Gets the ID of a custom portable collection in the same form as its owner's
|
|
3832
|
+
* ID: a portable ID if the owner has one, or otherwise a compatible identifier
|
|
3833
|
+
* on the owner's first gateway, as FEP-ef61 requires of publishers.
|
|
3834
|
+
*/
|
|
3835
|
+
function getCustomCollectionId(ownerId, owner, portableId) {
|
|
3836
|
+
if (isPortableUri(ownerId)) return portableId;
|
|
3837
|
+
const gateway = owner.gateways[0];
|
|
3838
|
+
if (gateway == null || !isGatewayUrl(gateway)) return portableId;
|
|
3839
|
+
try {
|
|
3840
|
+
return toCompatibleEf61Id(portableId, gateway);
|
|
3841
|
+
} catch (error) {
|
|
3842
|
+
if (error instanceof TypeError) return portableId;
|
|
3843
|
+
throw error;
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Builds the URI that the pages of a portable collection are relative to:
|
|
3848
|
+
* the collection's ID with the query parameters of the requested view, e.g.,
|
|
3849
|
+
* a filter, but without the page cursor and location hints.
|
|
3850
|
+
* @param collectionId The ID of the collection.
|
|
3851
|
+
* @param requestUrl The URL of the gateway request.
|
|
3852
|
+
* @returns The view URI.
|
|
3853
|
+
*/
|
|
3854
|
+
function buildPortableCollectionView(collectionId, requestUrl) {
|
|
3855
|
+
const view = new URL(collectionId);
|
|
3856
|
+
for (const [name, value] of requestUrl.searchParams) if (!NON_VIEW_PARAMETERS.has(name)) view.searchParams.append(name, value);
|
|
3857
|
+
return view;
|
|
3858
|
+
}
|
|
3859
|
+
//#endregion
|
|
3860
|
+
//#region src/federation/outgoing-proof.ts
|
|
3861
|
+
function formatKeyIds(candidates) {
|
|
3862
|
+
return candidates.map((c) => c.verificationMethod.href).join(", ");
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Rejects an activity of a portable actor whose ID cannot be authenticated by
|
|
3866
|
+
* the actor's DID: FEP-ef61 requires the activity to be a portable object
|
|
3867
|
+
* signed by that DID, so its ID has to be a portable ID, i.e., an `ap:` or
|
|
3868
|
+
* `ap+ef61:` URI or a compatible identifier, with the same DID. A portable
|
|
3869
|
+
* actor is one whose ID is such an ID as well.
|
|
3870
|
+
* @throws {TypeError} If the activity is performed by a portable actor, but
|
|
3871
|
+
* its ID is not a portable ID of the actor's DID, its
|
|
3872
|
+
* portable actors do not share a DID, or the ID of one of
|
|
3873
|
+
* them is a malformed compatible identifier.
|
|
3874
|
+
* @internal
|
|
3875
|
+
*/
|
|
3876
|
+
function assertPortableActorActivity(activity) {
|
|
3877
|
+
const portableActors = activity.actorIds.filter((id) => isPortableId(id));
|
|
3878
|
+
if (portableActors.length < 1) return;
|
|
3879
|
+
const dids = portableActors.map((actorId) => {
|
|
3880
|
+
const did = getPortableDid(actorId);
|
|
3881
|
+
if (did == null) throw new TypeError(`The activity ${activity.id?.href} is performed by the actor ${actorId.href}, whose ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
3882
|
+
return did;
|
|
3883
|
+
});
|
|
3884
|
+
const did = dids[0];
|
|
3885
|
+
if (dids.some((d) => d !== did)) throw new TypeError(`The activity ${activity.id?.href} has portable actors with different DIDs (${portableActors.map((a) => a.href).join(", ")}); a portable activity is signed by a single DID.`);
|
|
3886
|
+
if (activity.id == null || !isPortableId(activity.id) || getPortableDid(activity.id) !== did) throw new TypeError(`The activity ${activity.id?.href} is performed by the portable actor ${portableActors[0].href}, so its ID has to be an ap: or ap+ef61: URI, or an FEP-ef61 compatible identifier, with the same DID (${did}), as FEP-ef61 requires it to carry an Object Integrity Proof made by that DID.`);
|
|
3887
|
+
if (!isCompatibleEf61Iri(activity.id)) return;
|
|
3888
|
+
const mismatched = portableActors.find((actorId) => isCompatibleEf61Iri(actorId) && actorId.origin !== activity.id?.origin);
|
|
3889
|
+
if (mismatched == null) return;
|
|
3890
|
+
getLogger([
|
|
3891
|
+
"fedify",
|
|
3892
|
+
"federation",
|
|
3893
|
+
"outbox"
|
|
3894
|
+
]).warn("The activity {activityId} and its actor {actorId} are identified by FEP-ef61 compatible identifiers on different gateways. FEP-ef61 requires publishers to use the first gateway in the actor's gateways when constructing compatible identifiers.", {
|
|
3895
|
+
activityId: activity.id.href,
|
|
3896
|
+
actorId: mismatched.href
|
|
3897
|
+
});
|
|
3898
|
+
}
|
|
3899
|
+
/**
|
|
3900
|
+
* Creates the Object Integrity Proofs Fedify attaches to an outgoing activity.
|
|
3901
|
+
*
|
|
3902
|
+
* Keys whose IDs are FEP-ef61 compatible identifiers are gateway keys, which
|
|
3903
|
+
* never sign proofs. A portable activity, i.e., one with an `ap:` or
|
|
3904
|
+
* `ap+ef61:` ID or a compatible identifier, that carries no proof is signed only by the one Ed25519 key
|
|
3905
|
+
* whose verification method is a DID URL for its DID, even if it is the only
|
|
3906
|
+
* key; if no key or more than one key qualifies, this function throws.
|
|
3907
|
+
*
|
|
3908
|
+
* Outside the map-local compound-proof profile, i.e., when the activity
|
|
3909
|
+
* embeds no portable object, every Ed25519 key signs the activity, as it
|
|
3910
|
+
* always has. Inside the profile, which accepts exactly one direct proof per
|
|
3911
|
+
* map, Fedify creates at most one proof:
|
|
3912
|
+
*
|
|
3913
|
+
* - An activity that already carries a proof is kept as is.
|
|
3914
|
+
* - A single Ed25519 key signs a non-portable activity.
|
|
3915
|
+
* - With several Ed25519 keys, a non-portable activity is ambiguous, and
|
|
3916
|
+
* this function throws.
|
|
3917
|
+
*
|
|
3918
|
+
* @throws {TypeError} If a single proof key cannot be chosen, or the activity
|
|
3919
|
+
* is performed by a portable actor but is not a portable
|
|
3920
|
+
* activity of the same DID.
|
|
3921
|
+
* @internal
|
|
3922
|
+
*/
|
|
3923
|
+
async function signOutgoingActivity(activity, candidates, options) {
|
|
3924
|
+
const { contextLoader, tracerProvider } = options;
|
|
3925
|
+
assertPortableActorActivity(activity);
|
|
3926
|
+
const sign = async (selected) => {
|
|
3927
|
+
for (const { verificationMethod, privateKey } of selected) activity = await signObject(activity, privateKey, verificationMethod, {
|
|
3928
|
+
contextLoader,
|
|
3929
|
+
tracerProvider
|
|
3930
|
+
});
|
|
3931
|
+
return {
|
|
3932
|
+
activity,
|
|
3933
|
+
hasProof: true,
|
|
3934
|
+
proofCreated: true
|
|
3935
|
+
};
|
|
3936
|
+
};
|
|
3937
|
+
let hasProof = false;
|
|
3938
|
+
for await (const _ of activity.getProofs({ contextLoader })) {
|
|
3939
|
+
hasProof = true;
|
|
3940
|
+
break;
|
|
3941
|
+
}
|
|
3942
|
+
const keys = candidates.filter((c) => c.privateKey.algorithm.name === "Ed25519" && !isPortableKeyId(c.verificationMethod));
|
|
3943
|
+
if (!hasProof && activity.id != null && isPortableId(activity.id)) return await sign(selectPortableActivityKey(activity, activity.id, keys));
|
|
3944
|
+
if (keys.length < 1 || hasProof && !options.appendToExistingProofs) return {
|
|
3945
|
+
activity,
|
|
3946
|
+
hasProof,
|
|
3947
|
+
proofCreated: false
|
|
3948
|
+
};
|
|
3949
|
+
if (keys.length < 2 && !hasProof) return await sign(keys);
|
|
3950
|
+
if (!containsCompoundPortableObject(await activity.toJsonLd({
|
|
3951
|
+
format: "compact",
|
|
3952
|
+
contextLoader
|
|
3953
|
+
}))) return await sign(keys);
|
|
3954
|
+
const logger = getLogger([
|
|
3955
|
+
"fedify",
|
|
3956
|
+
"federation",
|
|
3957
|
+
"outbox"
|
|
3958
|
+
]);
|
|
3959
|
+
const activityId = activity.id?.href;
|
|
3960
|
+
if (hasProof) {
|
|
3961
|
+
logger.debug("The activity {activityId} embeds portable objects and already carries a proof, so no further proof is added; the map-local compound-proof profile accepts exactly one direct proof per map.", { activityId });
|
|
3962
|
+
return {
|
|
3963
|
+
activity,
|
|
3964
|
+
hasProof,
|
|
3965
|
+
proofCreated: false
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
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.`);
|
|
3969
|
+
}
|
|
3970
|
+
/**
|
|
3971
|
+
* Chooses the one Ed25519 key that can sign a portable activity: the key
|
|
3972
|
+
* whose verification method is a DID URL for the activity's DID.
|
|
3973
|
+
* @throws {TypeError} If no key or more than one key qualifies.
|
|
3974
|
+
*/
|
|
3975
|
+
function selectPortableActivityKey(activity, activityId, keys) {
|
|
3976
|
+
const activityDid = getPortableDid(activityId);
|
|
3977
|
+
if (activityDid == null) throw new TypeError(`Cannot sign the portable activity ${activityId.href}, as its ID is a malformed FEP-ef61 portable ID. A compatible identifier must have a valid DID and an object path, and must not have credentials or @gateway location hints.`);
|
|
3978
|
+
const eligible = keys.filter((c) => c.verificationMethod.protocol === "did:" && getPortableDid(c.verificationMethod) === activityDid);
|
|
3979
|
+
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.`);
|
|
3980
|
+
getLogger([
|
|
3981
|
+
"fedify",
|
|
3982
|
+
"federation",
|
|
3983
|
+
"outbox"
|
|
3984
|
+
]).debug("The portable activity {activityId} is signed only by {keyId}, the one Ed25519 key whose verification method matches its DID.", {
|
|
3985
|
+
activityId: activity.id?.href,
|
|
3986
|
+
keyId: eligible[0].verificationMethod.href
|
|
3987
|
+
});
|
|
3988
|
+
return eligible;
|
|
3989
|
+
}
|
|
3990
|
+
/**
|
|
3991
|
+
* Rejects an outgoing activity whose proofs Fedify's own inbox would reject
|
|
3992
|
+
* as unsupported: a document with portable objects in which some map carries
|
|
3993
|
+
* a proof set or another value that is not a single proof map, or in which
|
|
3994
|
+
* an embedded map carries a proof but not its own `@context`, e.g., a signed
|
|
3995
|
+
* object parsed from a received document and rebuilt under the activity's
|
|
3996
|
+
* context.
|
|
3997
|
+
*
|
|
3998
|
+
* @param jsonLd The compact JSON-LD document about to be delivered.
|
|
3999
|
+
* @param activityId The activity ID, for the error message.
|
|
4000
|
+
* @throws {TypeError} If the document has an unsupported proof shape.
|
|
4001
|
+
* @internal
|
|
4002
|
+
*/
|
|
4003
|
+
function assertSupportedCompoundProofShape(jsonLd, activityId) {
|
|
4004
|
+
if (!containsCompoundPortableObject(jsonLd)) return;
|
|
4005
|
+
const path = findUnsupportedCompoundProofShape(jsonLd);
|
|
4006
|
+
if (path != null) throw new TypeError(`Cannot send the activity ${activityId}: it embeds portable objects, but its proof at the JSON Pointer ${JSON.stringify(path)} is not a single proof map. The map-local compound-proof profile accepts exactly one direct proof per map, so Fedify inboxes would reject the activity. Sign each object with exactly one key.`);
|
|
4007
|
+
const embedded = findEmbeddedProofWithoutContext(jsonLd);
|
|
4008
|
+
if (embedded != null) throw new TypeError(`Cannot send the activity ${activityId}: it embeds portable objects, but the map at the JSON Pointer ${JSON.stringify(embedded)} carries a proof without its own @context. The map-local compound-proof profile verifies each proof-bearing map on its own, so Fedify inboxes would reject the activity. This happens when a signed object parsed from a received document, e.g., a Follow in an inbox listener, is embedded, as it is rebuilt under the activity's context and its proof no longer covers it. Refer to such an object by its ID instead, e.g., new Accept({ object: follow.id }), or embed an object that you signed with signObject().`);
|
|
2272
4009
|
}
|
|
2273
|
-
const DEF_COMPONENTS = [
|
|
2274
|
-
"@method",
|
|
2275
|
-
"@target-uri",
|
|
2276
|
-
"@authority",
|
|
2277
|
-
"content-digest"
|
|
2278
|
-
];
|
|
2279
|
-
const MIN_COMPONENTS = [
|
|
2280
|
-
"@method",
|
|
2281
|
-
"@target-uri",
|
|
2282
|
-
"@authority"
|
|
2283
|
-
];
|
|
2284
4010
|
//#endregion
|
|
2285
4011
|
//#region src/federation/tasks/enqueue.ts
|
|
2286
4012
|
/**
|
|
@@ -2348,8 +4074,8 @@ function planDeduplication(ctx, queue, taskName, options, itemCount) {
|
|
|
2348
4074
|
const key = options.deduplicationKey;
|
|
2349
4075
|
const native = queue.nativeDeduplication === true;
|
|
2350
4076
|
const canCas = ctx.federation.kv.cas != null;
|
|
2351
|
-
const canBatchAtomically = queue.enqueueMany != null &&
|
|
2352
|
-
if (itemCount > 1 && !canBatchAtomically && (native || canCas)) throw new TypeError(`Task ${JSON.stringify(taskName)} was enqueued as a batch with a deduplicationKey, but its message queue does not
|
|
4077
|
+
const canBatchAtomically = queue.enqueueMany != null && queue.atomicEnqueueMany !== false;
|
|
4078
|
+
if (itemCount > 1 && !canBatchAtomically && (native || canCas)) throw new TypeError(`Task ${JSON.stringify(taskName)} was enqueued as a batch with a deduplicationKey, but its message queue does not provide an atomic enqueueMany implementation; a multi-item batch cannot be deduplicated atomically without it. Use a queue with atomic batch enqueueing, or enqueue the tasks individually with enqueueTask().`);
|
|
2353
4079
|
if (native) return {
|
|
2354
4080
|
kind: "native",
|
|
2355
4081
|
key
|
|
@@ -2481,6 +4207,73 @@ const logger = getLogger([
|
|
|
2481
4207
|
"webfinger",
|
|
2482
4208
|
"server"
|
|
2483
4209
|
]);
|
|
4210
|
+
function getWebFingerSubjectAndAliases({ resourceUrl, actorUri, preferredUsername, acctUsername, host, contextHost }) {
|
|
4211
|
+
const aliases = [];
|
|
4212
|
+
let subject = resourceUrl.href;
|
|
4213
|
+
if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
|
|
4214
|
+
aliases.push(`acct:${preferredUsername}@${host ?? contextHost}`);
|
|
4215
|
+
if (host != null && host !== contextHost) aliases.push(`acct:${preferredUsername}@${contextHost}`);
|
|
4216
|
+
}
|
|
4217
|
+
if (resourceUrl.href !== actorUri.href) aliases.push(actorUri.href);
|
|
4218
|
+
if (resourceUrl.protocol === "acct:" && host != null && host !== contextHost && !resourceUrl.href.endsWith(`@${host}`)) {
|
|
4219
|
+
subject = `acct:${preferredUsername ?? acctUsername}@${host}`;
|
|
4220
|
+
aliases.push(resourceUrl.href);
|
|
4221
|
+
if (preferredUsername != null && preferredUsername !== "" && preferredUsername !== acctUsername) aliases.push(`acct:${preferredUsername}@${contextHost}`);
|
|
4222
|
+
}
|
|
4223
|
+
return {
|
|
4224
|
+
subject,
|
|
4225
|
+
aliases
|
|
4226
|
+
};
|
|
4227
|
+
}
|
|
4228
|
+
/**
|
|
4229
|
+
* Gets the compatible identifier that a WebFinger response links to for
|
|
4230
|
+
* an FEP-ef61 portable actor. An actor identified by an `ap:` or `ap+ef61:`
|
|
4231
|
+
* URI is linked through its compatible identifier on the given gateway, while
|
|
4232
|
+
* an actor identified by a compatible identifier is linked through its ID as
|
|
4233
|
+
* is, so that software without FEP-ef61 support finds the same ID in the actor
|
|
4234
|
+
* document.
|
|
4235
|
+
* @param actorId The portable actor's ID.
|
|
4236
|
+
* @param gateway The actor's first gateway.
|
|
4237
|
+
* @returns The compatible identifier, or `null` if the actor ID is malformed
|
|
4238
|
+
* or cannot be represented as a compatible identifier.
|
|
4239
|
+
*/
|
|
4240
|
+
function getCompatibleActorId(actorId, gateway) {
|
|
4241
|
+
try {
|
|
4242
|
+
if (isPortableUri(actorId)) return toCompatibleEf61Id(actorId, gateway);
|
|
4243
|
+
return fromCompatibleEf61Id(actorId) == null ? null : actorId;
|
|
4244
|
+
} catch (error) {
|
|
4245
|
+
if (error instanceof TypeError) return null;
|
|
4246
|
+
throw error;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Computes the subject and aliases for an FEP-ef61 portable actor, whose
|
|
4251
|
+
* WebFinger address takes its domain from the first gateway of the actor
|
|
4252
|
+
* rather than from the actor ID. The canonical `ap:` ID is not emitted,
|
|
4253
|
+
* since it is not a valid URI for software that does not support FEP-ef61;
|
|
4254
|
+
* the compatible identifier stands for it.
|
|
4255
|
+
*/
|
|
4256
|
+
function getPortableWebFingerSubjectAndAliases({ resourceUrl, compatibleId, preferredUsername, acctUsername, gatewayHost }) {
|
|
4257
|
+
const aliases = [];
|
|
4258
|
+
const add = (alias) => {
|
|
4259
|
+
if (alias !== subject && !aliases.includes(alias)) aliases.push(alias);
|
|
4260
|
+
};
|
|
4261
|
+
let subject;
|
|
4262
|
+
if (resourceUrl.protocol === "acct:") {
|
|
4263
|
+
const username = preferredUsername ?? acctUsername;
|
|
4264
|
+
subject = username == null ? resourceUrl.href : `acct:${username}@${gatewayHost}`;
|
|
4265
|
+
add(compatibleId.href);
|
|
4266
|
+
add(resourceUrl.href);
|
|
4267
|
+
} else {
|
|
4268
|
+
subject = preferredUsername == null ? compatibleId.href : `acct:${preferredUsername}@${gatewayHost}`;
|
|
4269
|
+
add(compatibleId.href);
|
|
4270
|
+
if (resourceUrl.protocol === "http:" || resourceUrl.protocol === "https:") add(resourceUrl.href);
|
|
4271
|
+
}
|
|
4272
|
+
return {
|
|
4273
|
+
subject,
|
|
4274
|
+
aliases
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
2484
4277
|
/**
|
|
2485
4278
|
* Handles a WebFinger request. You would not typically call this function
|
|
2486
4279
|
* directly, but instead use {@link Federation.fetch} method.
|
|
@@ -2529,7 +4322,7 @@ async function handleWebFinger(request, options) {
|
|
|
2529
4322
|
});
|
|
2530
4323
|
}
|
|
2531
4324
|
}
|
|
2532
|
-
const WEBFINGER_HANDLE_SCHEME_WHITELIST = new Set([
|
|
4325
|
+
const WEBFINGER_HANDLE_SCHEME_WHITELIST = /* @__PURE__ */ new Set([
|
|
2533
4326
|
"acct",
|
|
2534
4327
|
"http",
|
|
2535
4328
|
"https",
|
|
@@ -2566,7 +4359,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2566
4359
|
span?.setAttribute("webfinger.resource", resource);
|
|
2567
4360
|
let resourceUrl;
|
|
2568
4361
|
try {
|
|
2569
|
-
resourceUrl =
|
|
4362
|
+
resourceUrl = parseIri(resource);
|
|
2570
4363
|
} catch (e) {
|
|
2571
4364
|
if (e instanceof TypeError) return new Response("Invalid resource URL.", { status: 400 });
|
|
2572
4365
|
throw e;
|
|
@@ -2585,6 +4378,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2585
4378
|
return identifier;
|
|
2586
4379
|
}
|
|
2587
4380
|
let identifier = null;
|
|
4381
|
+
let acctUsername = null;
|
|
2588
4382
|
const uriParsed = context.parseUri(resourceUrl);
|
|
2589
4383
|
if (uriParsed?.type != "actor") {
|
|
2590
4384
|
const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
|
|
@@ -2598,8 +4392,9 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2598
4392
|
const normalizedHost = portMatch == null ? domainToASCII(match[2].toLowerCase()) : domainToASCII(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0];
|
|
2599
4393
|
if (normalizedHost != context.url.host && normalizedHost != host) return await onNotFound(request);
|
|
2600
4394
|
else {
|
|
2601
|
-
|
|
2602
|
-
|
|
4395
|
+
acctUsername = match[1];
|
|
4396
|
+
identifier = await mapUsernameToIdentifier(acctUsername);
|
|
4397
|
+
resourceUrl = new URL(`acct:${acctUsername}@${normalizedHost}`);
|
|
2603
4398
|
}
|
|
2604
4399
|
}
|
|
2605
4400
|
} else identifier = uriParsed.identifier;
|
|
@@ -2613,9 +4408,34 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2613
4408
|
status: 410,
|
|
2614
4409
|
headers: { "Access-Control-Allow-Origin": "*" }
|
|
2615
4410
|
});
|
|
4411
|
+
let actorUri = context.getActorUri(identifier);
|
|
4412
|
+
let portable;
|
|
4413
|
+
if (actor.id != null && isPortableId(actor.id)) {
|
|
4414
|
+
const gateway = actor.gateway;
|
|
4415
|
+
if (gateway == null || !isGatewayUrl(gateway)) {
|
|
4416
|
+
logger.error("The portable actor {actorId} (identifier: {identifier}) needs an HTTP(S) origin as the first item of its gateways property to be discovered through WebFinger.", {
|
|
4417
|
+
actorId: formatIri(actor.id),
|
|
4418
|
+
identifier
|
|
4419
|
+
});
|
|
4420
|
+
return await onNotFound(request);
|
|
4421
|
+
}
|
|
4422
|
+
const compatibleId = getCompatibleActorId(actor.id, gateway);
|
|
4423
|
+
if (compatibleId == null) {
|
|
4424
|
+
logger.error("The portable actor {actorId} (identifier: {identifier}) cannot be discovered through WebFinger, as its ID is malformed or cannot be represented as an FEP-ef61 compatible identifier.", {
|
|
4425
|
+
actorId: formatIri(actor.id),
|
|
4426
|
+
identifier
|
|
4427
|
+
});
|
|
4428
|
+
return await onNotFound(request);
|
|
4429
|
+
}
|
|
4430
|
+
actorUri = compatibleId;
|
|
4431
|
+
portable = {
|
|
4432
|
+
compatibleId,
|
|
4433
|
+
gatewayHost: gateway.host
|
|
4434
|
+
};
|
|
4435
|
+
}
|
|
2616
4436
|
const links = [{
|
|
2617
4437
|
rel: "self",
|
|
2618
|
-
href:
|
|
4438
|
+
href: actorUri.href,
|
|
2619
4439
|
type: "application/activity+json"
|
|
2620
4440
|
}];
|
|
2621
4441
|
for (const url of actor.urls) if (url instanceof Link && url.href != null) links.push({
|
|
@@ -2639,23 +4459,25 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2639
4459
|
const customLinks = await webFingerLinksDispatcher(context, resourceUrl);
|
|
2640
4460
|
if (customLinks != null) for (const link of customLinks) links.push(link);
|
|
2641
4461
|
}
|
|
2642
|
-
const aliases =
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
4462
|
+
const { subject, aliases } = portable == null ? getWebFingerSubjectAndAliases({
|
|
4463
|
+
resourceUrl,
|
|
4464
|
+
actorUri,
|
|
4465
|
+
preferredUsername: actor.preferredUsername,
|
|
4466
|
+
acctUsername,
|
|
4467
|
+
host,
|
|
4468
|
+
contextHost: context.url.host
|
|
4469
|
+
}) : getPortableWebFingerSubjectAndAliases({
|
|
4470
|
+
resourceUrl,
|
|
4471
|
+
compatibleId: portable.compatibleId,
|
|
4472
|
+
preferredUsername: actor.preferredUsername,
|
|
4473
|
+
acctUsername,
|
|
4474
|
+
gatewayHost: portable.gatewayHost
|
|
4475
|
+
});
|
|
4476
|
+
return new Response(JSON.stringify({
|
|
4477
|
+
subject,
|
|
2655
4478
|
aliases,
|
|
2656
4479
|
links
|
|
2657
|
-
}
|
|
2658
|
-
return new Response(JSON.stringify(jrd), { headers: {
|
|
4480
|
+
}), { headers: {
|
|
2659
4481
|
"Content-Type": "application/jrd+json",
|
|
2660
4482
|
"Access-Control-Allow-Origin": "*"
|
|
2661
4483
|
} });
|
|
@@ -2743,6 +4565,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
|
|
|
2743
4565
|
function maxDelay(first, second) {
|
|
2744
4566
|
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
2745
4567
|
}
|
|
4568
|
+
/**
|
|
4569
|
+
* Gets the fields of a queued message that describe the FEP-ef61 portable
|
|
4570
|
+
* inbox an inbox stands for, if any, omitting the absent ones.
|
|
4571
|
+
*/
|
|
4572
|
+
function getPortableMessageFields(portableInbox, gatewayInboxes) {
|
|
4573
|
+
return {
|
|
4574
|
+
...portableInbox == null ? {} : { portableInbox },
|
|
4575
|
+
...gatewayInboxes == null ? {} : { gatewayInboxes: [...gatewayInboxes] }
|
|
4576
|
+
};
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Sends an activity to an inbox, or, if the inbox is the compatible
|
|
4580
|
+
* identifier of an FEP-ef61 portable inbox on more than one gateway, through
|
|
4581
|
+
* the first gateway that accepts it.
|
|
4582
|
+
*/
|
|
4583
|
+
function sendActivityToInbox(inbox, gatewayInboxes, parameters) {
|
|
4584
|
+
if (gatewayInboxes == null || gatewayInboxes.length < 2) return sendActivity({
|
|
4585
|
+
...parameters,
|
|
4586
|
+
inbox: new URL(inbox)
|
|
4587
|
+
});
|
|
4588
|
+
return sendActivityThroughGateways(parameters, gatewayInboxes.map((i) => new URL(i)));
|
|
4589
|
+
}
|
|
4590
|
+
function parseInstant(value) {
|
|
4591
|
+
if (value == null) return void 0;
|
|
4592
|
+
try {
|
|
4593
|
+
return Temporal.Instant.from(value);
|
|
4594
|
+
} catch (error) {
|
|
4595
|
+
if (error instanceof RangeError) return void 0;
|
|
4596
|
+
throw error;
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
2746
4599
|
function isTransportDeliveryError(error) {
|
|
2747
4600
|
return error instanceof FetchError || isAbortError(error);
|
|
2748
4601
|
}
|
|
@@ -2772,7 +4625,7 @@ function isPermanentRemoteContextError(error) {
|
|
|
2772
4625
|
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
2773
4626
|
}
|
|
2774
4627
|
function isPermanentInboxParseError(error) {
|
|
2775
|
-
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
4628
|
+
return error instanceof Error && (error.name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError(error) || error.name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure(error)) || error instanceof TypeError && (/^(Invalid JSON-LD:|Invalid type:|Unexpected type:|Invalid @id:|Invalid FEP-ef61 gateway:)/.test(error.message) || isInvalidUrlTypeError(error));
|
|
2776
4629
|
}
|
|
2777
4630
|
function hasLinkedDataSignatureJsonLdProcessingError(error) {
|
|
2778
4631
|
if (error.message.startsWith("Maximum deep iterations exceeded")) return true;
|
|
@@ -2789,6 +4642,8 @@ function isLinkedDataSignatureJsonLdProcessingError(error) {
|
|
|
2789
4642
|
* @returns A new {@link Federation} instance.
|
|
2790
4643
|
* @throws {TypeError} If benchmark mode and `meterProvider` are both
|
|
2791
4644
|
* specified.
|
|
4645
|
+
* @throws {RangeError} If {@link FederationOptions.maxHttpSignatures} is not
|
|
4646
|
+
* a positive integer or `Infinity`.
|
|
2792
4647
|
* @since 0.10.0
|
|
2793
4648
|
*/
|
|
2794
4649
|
function createFederation(options) {
|
|
@@ -2797,6 +4652,9 @@ function createFederation(options) {
|
|
|
2797
4652
|
var FederationImpl = class extends FederationBuilderImpl {
|
|
2798
4653
|
kv;
|
|
2799
4654
|
kvPrefixes;
|
|
4655
|
+
publicKeyTtl;
|
|
4656
|
+
httpMessageSignaturesSpecTtl;
|
|
4657
|
+
portableInboxForwarding;
|
|
2800
4658
|
inboxQueue;
|
|
2801
4659
|
outboxQueue;
|
|
2802
4660
|
fanoutQueue;
|
|
@@ -2812,6 +4670,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2812
4670
|
onOutboxError;
|
|
2813
4671
|
permanentFailureStatusCodes;
|
|
2814
4672
|
signatureTimeWindow;
|
|
4673
|
+
maxHttpSignatures;
|
|
2815
4674
|
skipSignatureVerification;
|
|
2816
4675
|
outboxRetryPolicy;
|
|
2817
4676
|
inboxRetryPolicy;
|
|
@@ -2829,6 +4688,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2829
4688
|
benchmarkMetricReader;
|
|
2830
4689
|
benchmarkTriggerOptions;
|
|
2831
4690
|
#mediaUploaderNoAuthWarned = false;
|
|
4691
|
+
#portableInboxForwardingWarned = false;
|
|
2832
4692
|
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
2833
4693
|
#queueDepthGaugeEntries = [];
|
|
2834
4694
|
#queueDepthGaugeMeterProvider;
|
|
@@ -2839,6 +4699,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2839
4699
|
const hasCustomLoaderFactory = options.documentLoaderFactory != null || options.contextLoaderFactory != null;
|
|
2840
4700
|
const allowPrivateAddress = options.allowPrivateAddress ?? (benchmarkMode && !hasCustomLoaderFactory ? true : false);
|
|
2841
4701
|
const signatureTimeWindow = options.signatureTimeWindow ?? (benchmarkMode ? false : { hours: 1 });
|
|
4702
|
+
if (options.maxHttpSignatures !== void 0) validateMaxSignatures(options.maxHttpSignatures, "maxHttpSignatures");
|
|
2842
4703
|
if (benchmarkMode && options.meterProvider != null) throw new TypeError("benchmarkMode requires Fedify to own the meterProvider; OpenTelemetry metric readers cannot be added after a MeterProvider is constructed.");
|
|
2843
4704
|
if (benchmarkMode) {
|
|
2844
4705
|
const relaxations = getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow);
|
|
@@ -2860,8 +4721,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2860
4721
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2861
4722
|
circuitBreaker: ["_fedify", "circuit"],
|
|
2862
4723
|
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
4724
|
+
portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
|
|
2863
4725
|
...options.kvPrefixes ?? {}
|
|
2864
4726
|
};
|
|
4727
|
+
this.publicKeyTtl = Temporal.Duration.from(options.publicKeyTtl ?? { days: 30 });
|
|
4728
|
+
this.httpMessageSignaturesSpecTtl = Temporal.Duration.from(options.httpMessageSignaturesSpecTtl ?? { days: 90 });
|
|
4729
|
+
this.portableInboxForwarding = resolvePortableInboxForwardingOptions(options.portableInboxForwarding);
|
|
2865
4730
|
if (options.queue == null) {
|
|
2866
4731
|
this.inboxQueue = void 0;
|
|
2867
4732
|
this.outboxQueue = void 0;
|
|
@@ -2927,13 +4792,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2927
4792
|
}
|
|
2928
4793
|
const { userAgent } = options;
|
|
2929
4794
|
this.allowPrivateAddress = allowPrivateAddress;
|
|
4795
|
+
const documentLoaderTimeout = resolveDocumentLoaderTimeout(options.documentLoaderTimeout == null ? options.documentLoaderTimeout : Temporal.Duration.from(options.documentLoaderTimeout).total("millisecond"));
|
|
2930
4796
|
const userDocumentLoaderFactory = options.documentLoaderFactory;
|
|
2931
4797
|
const userContextLoaderFactory = options.contextLoaderFactory;
|
|
2932
4798
|
const userAuthFactory = options.authenticatedDocumentLoaderFactory;
|
|
2933
4799
|
const builtinDocumentLoaderFactory = (opts) => kvCache({
|
|
2934
4800
|
loader: getDocumentLoader({
|
|
2935
4801
|
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2936
|
-
userAgent: opts?.userAgent ?? userAgent
|
|
4802
|
+
userAgent: opts?.userAgent ?? userAgent,
|
|
4803
|
+
timeout: opts?.timeout !== void 0 ? opts.timeout : documentLoaderTimeout
|
|
2937
4804
|
}),
|
|
2938
4805
|
kv: options.kv,
|
|
2939
4806
|
prefix: this.kvPrefixes.remoteDocument,
|
|
@@ -2943,7 +4810,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2943
4810
|
const builtinContextLoaderFactory = (opts) => kvCache({
|
|
2944
4811
|
loader: getDocumentLoader({
|
|
2945
4812
|
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2946
|
-
userAgent: opts?.userAgent ?? userAgent
|
|
4813
|
+
userAgent: opts?.userAgent ?? userAgent,
|
|
4814
|
+
timeout: opts?.timeout !== void 0 ? opts.timeout : documentLoaderTimeout
|
|
2947
4815
|
}),
|
|
2948
4816
|
kv: options.kv,
|
|
2949
4817
|
prefix: this.kvPrefixes.remoteDocument,
|
|
@@ -2964,7 +4832,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2964
4832
|
this.authenticatedDocumentLoaderFactory = (identity, factoryOpts) => instrumentDocumentLoader(userAuthFactory != null ? userAuthFactory(identity, factoryOpts) : getAuthenticatedDocumentLoader(identity, {
|
|
2965
4833
|
allowPrivateAddress: factoryOpts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2966
4834
|
userAgent: factoryOpts?.userAgent ?? userAgent,
|
|
2967
|
-
|
|
4835
|
+
timeout: factoryOpts?.timeout !== void 0 ? factoryOpts.timeout : documentLoaderTimeout,
|
|
4836
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
2968
4837
|
tracerProvider: this.tracerProvider
|
|
2969
4838
|
}), {
|
|
2970
4839
|
meterProvider: this._meterProvider,
|
|
@@ -2975,6 +4844,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2975
4844
|
this.onOutboxError = options.onOutboxError;
|
|
2976
4845
|
this.permanentFailureStatusCodes = options.permanentFailureStatusCodes ?? [404, 410];
|
|
2977
4846
|
this.signatureTimeWindow = signatureTimeWindow;
|
|
4847
|
+
this.maxHttpSignatures = options.maxHttpSignatures ?? 3;
|
|
2978
4848
|
this.skipSignatureVerification = options.skipSignatureVerification ?? false;
|
|
2979
4849
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
2980
4850
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
@@ -3207,6 +5077,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3207
5077
|
collectionSync: message.collectionSync,
|
|
3208
5078
|
orderingKey: message.orderingKey,
|
|
3209
5079
|
normalizeExistingProofs: message.normalizeExistingProofs,
|
|
5080
|
+
activityJsonLd: message.activity,
|
|
3210
5081
|
context
|
|
3211
5082
|
});
|
|
3212
5083
|
}
|
|
@@ -3247,11 +5118,16 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3247
5118
|
});
|
|
3248
5119
|
return parsedActorIds;
|
|
3249
5120
|
};
|
|
3250
|
-
const parseActivity = () =>
|
|
3251
|
-
contextLoader
|
|
3252
|
-
documentLoader
|
|
3253
|
-
|
|
3254
|
-
|
|
5121
|
+
const parseActivity = () => {
|
|
5122
|
+
const contextLoader = this.contextLoaderFactory(loaderOptions);
|
|
5123
|
+
const documentLoader = rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions);
|
|
5124
|
+
return Activity.fromJsonLd(message.activity, {
|
|
5125
|
+
contextLoader,
|
|
5126
|
+
documentLoader,
|
|
5127
|
+
tracerProvider: this.tracerProvider,
|
|
5128
|
+
verifyPortableObject: createPortableObjectVerifier(documentLoader, contextLoader, this)
|
|
5129
|
+
});
|
|
5130
|
+
};
|
|
3255
5131
|
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
3256
5132
|
const { outboxQueue } = this;
|
|
3257
5133
|
if (outboxQueue == null) return;
|
|
@@ -3300,6 +5176,20 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3300
5176
|
}
|
|
3301
5177
|
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
3302
5178
|
};
|
|
5179
|
+
const { gatewayInboxes } = message;
|
|
5180
|
+
if (gatewayInboxes != null && gatewayInboxes.length > 1) {
|
|
5181
|
+
await this.#listenGatewayOutboxMessage(_, {
|
|
5182
|
+
...message,
|
|
5183
|
+
gatewayInboxes
|
|
5184
|
+
}, span, {
|
|
5185
|
+
keys,
|
|
5186
|
+
logData,
|
|
5187
|
+
getActorIds,
|
|
5188
|
+
parseActivity,
|
|
5189
|
+
dropHeldOutboxMessage
|
|
5190
|
+
});
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
3303
5193
|
try {
|
|
3304
5194
|
const inbox = new URL(message.inbox);
|
|
3305
5195
|
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
@@ -3332,6 +5222,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3332
5222
|
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
3333
5223
|
}
|
|
3334
5224
|
await sendActivity({
|
|
5225
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
3335
5226
|
keys,
|
|
3336
5227
|
activity: message.activity,
|
|
3337
5228
|
activityId: message.activityId,
|
|
@@ -3339,7 +5230,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3339
5230
|
inbox,
|
|
3340
5231
|
sharedInbox: message.sharedInbox,
|
|
3341
5232
|
headers: new Headers(message.headers),
|
|
3342
|
-
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
5233
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
3343
5234
|
meterProvider: this.meterProvider,
|
|
3344
5235
|
tracerProvider: this.tracerProvider
|
|
3345
5236
|
});
|
|
@@ -3521,6 +5412,308 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3521
5412
|
}
|
|
3522
5413
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
|
3523
5414
|
}
|
|
5415
|
+
/**
|
|
5416
|
+
* Delivers a queued activity to an FEP-ef61 portable inbox through the first
|
|
5417
|
+
* of its gateways that accepts it, trying them one after another in a single
|
|
5418
|
+
* round, so that the round counts as one attempt for the retry policy, keeps
|
|
5419
|
+
* the message's place in its ordering key, and is replayed as a whole by
|
|
5420
|
+
* queues that retry natively.
|
|
5421
|
+
*
|
|
5422
|
+
* A gateway that fails with a permanent failure status is not tried again.
|
|
5423
|
+
* A gateway whose circuit is open, or which asked to be retried later with
|
|
5424
|
+
* `Retry-After`, is skipped in this round. If no gateway accepts the
|
|
5425
|
+
* activity, the message is held while every remaining gateway is held, and
|
|
5426
|
+
* otherwise retried according to the retry policy.
|
|
5427
|
+
*/
|
|
5428
|
+
async #listenGatewayOutboxMessage(contextData, message, span, helpers) {
|
|
5429
|
+
const logger = getLogger([
|
|
5430
|
+
"fedify",
|
|
5431
|
+
"federation",
|
|
5432
|
+
"outbox"
|
|
5433
|
+
]);
|
|
5434
|
+
const circuitLogger = getLogger([
|
|
5435
|
+
"fedify",
|
|
5436
|
+
"federation",
|
|
5437
|
+
"circuit"
|
|
5438
|
+
]);
|
|
5439
|
+
const { keys, logData, getActorIds, parseActivity, dropHeldOutboxMessage } = helpers;
|
|
5440
|
+
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
5441
|
+
const specDeterminer = new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl });
|
|
5442
|
+
const survivors = [];
|
|
5443
|
+
let lastError;
|
|
5444
|
+
let lastPermanent;
|
|
5445
|
+
let activity;
|
|
5446
|
+
const getActivity = async () => activity ??= await parseActivity();
|
|
5447
|
+
const drop = async (circuit, remoteHost, inbox, heldSince) => {
|
|
5448
|
+
await dropHeldOutboxMessage(circuit, remoteHost, inbox, heldSince, await getActivity());
|
|
5449
|
+
};
|
|
5450
|
+
const heldSince = parseInstant(message.circuitHeldSince);
|
|
5451
|
+
if (circuit != null && heldSince != null && Temporal.Instant.compare(heldSince.add(circuit.options.heldActivityTtl), Temporal.Now.instant()) <= 0) {
|
|
5452
|
+
const inbox = new URL(message.gatewayInboxes[0]);
|
|
5453
|
+
await drop(circuit, getRemoteHost(inbox), inbox, heldSince);
|
|
5454
|
+
return;
|
|
5455
|
+
}
|
|
5456
|
+
for (const inboxHref of message.gatewayInboxes) {
|
|
5457
|
+
const inbox = new URL(inboxHref);
|
|
5458
|
+
const remoteHost = getRemoteHost(inbox);
|
|
5459
|
+
const now = Temporal.Now.instant();
|
|
5460
|
+
const notBefore = parseInstant(message.gatewayNotBefore?.[inboxHref]);
|
|
5461
|
+
if (notBefore != null && Temporal.Instant.compare(notBefore, now) > 0) {
|
|
5462
|
+
survivors.push({
|
|
5463
|
+
inbox: inboxHref,
|
|
5464
|
+
holdDelay: now.until(notBefore),
|
|
5465
|
+
notBefore
|
|
5466
|
+
});
|
|
5467
|
+
continue;
|
|
5468
|
+
}
|
|
5469
|
+
let decision;
|
|
5470
|
+
if (circuit != null) try {
|
|
5471
|
+
decision = await circuit.beforeSend(remoteHost, message);
|
|
5472
|
+
} catch (circuitError) {
|
|
5473
|
+
circuitLogger.error("Failed to check circuit breaker state before sending; proceeding with delivery:\n{error}", {
|
|
5474
|
+
...logData,
|
|
5475
|
+
remoteHost,
|
|
5476
|
+
error: circuitError
|
|
5477
|
+
});
|
|
5478
|
+
}
|
|
5479
|
+
if (decision != null && circuit != null) {
|
|
5480
|
+
if (decision.type === "drop") {
|
|
5481
|
+
await drop(circuit, remoteHost, inbox, decision.heldSince);
|
|
5482
|
+
return;
|
|
5483
|
+
} else if (decision.type === "hold") {
|
|
5484
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, decision.state);
|
|
5485
|
+
survivors.push({
|
|
5486
|
+
inbox: inboxHref,
|
|
5487
|
+
holdDelay: decision.delay,
|
|
5488
|
+
circuitHeld: true
|
|
5489
|
+
});
|
|
5490
|
+
continue;
|
|
5491
|
+
} else if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
5492
|
+
}
|
|
5493
|
+
try {
|
|
5494
|
+
await sendActivity({
|
|
5495
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
5496
|
+
keys,
|
|
5497
|
+
activity: message.activity,
|
|
5498
|
+
activityId: message.activityId,
|
|
5499
|
+
activityType: message.activityType,
|
|
5500
|
+
inbox,
|
|
5501
|
+
sharedInbox: message.sharedInbox,
|
|
5502
|
+
headers: new Headers(message.headers),
|
|
5503
|
+
specDeterminer,
|
|
5504
|
+
meterProvider: this.meterProvider,
|
|
5505
|
+
tracerProvider: this.tracerProvider
|
|
5506
|
+
});
|
|
5507
|
+
} catch (error) {
|
|
5508
|
+
lastError = error;
|
|
5509
|
+
const isPermanentFailure = error instanceof SendActivityError && this.permanentFailureStatusCodes.includes(error.statusCode);
|
|
5510
|
+
let retryAfter = !isPermanentFailure && error instanceof SendActivityError && (error.statusCode === 429 || error.statusCode === 503) ? parseRetryAfter(error.responseHeaders) : void 0;
|
|
5511
|
+
let retryAt;
|
|
5512
|
+
if (retryAfter != null) try {
|
|
5513
|
+
retryAt = Temporal.Now.instant().add(retryAfter);
|
|
5514
|
+
} catch (e) {
|
|
5515
|
+
if (!(e instanceof RangeError)) throw e;
|
|
5516
|
+
retryAfter = void 0;
|
|
5517
|
+
}
|
|
5518
|
+
let postDecision;
|
|
5519
|
+
if (circuit != null) try {
|
|
5520
|
+
if (error instanceof SendActivityError) {
|
|
5521
|
+
const { statusCode } = error;
|
|
5522
|
+
const stateChange = isPermanentFailure || statusCode === 429 || statusCode >= 400 && statusCode < 500 ? await circuit.recordReachableFailure(remoteHost) : statusCode >= 500 ? await circuit.recordFailure(remoteHost) : void 0;
|
|
5523
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
5524
|
+
} else if (isTransportDeliveryError(error)) {
|
|
5525
|
+
const stateChange = await circuit.recordFailure(remoteHost);
|
|
5526
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
5527
|
+
}
|
|
5528
|
+
if (!isPermanentFailure) postDecision = await circuit.beforeSend(remoteHost, message);
|
|
5529
|
+
} catch (circuitError) {
|
|
5530
|
+
circuitLogger.error("Failed to update circuit breaker state after delivery failure; falling back to normal failure handling:\n{error}", {
|
|
5531
|
+
...logData,
|
|
5532
|
+
remoteHost,
|
|
5533
|
+
error: circuitError
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
span.addEvent("activitypub.delivery.failed", {
|
|
5537
|
+
"activitypub.remote.host": remoteHost,
|
|
5538
|
+
"activitypub.delivery.attempt": message.attempt,
|
|
5539
|
+
"activitypub.delivery.permanent_failure": isPermanentFailure,
|
|
5540
|
+
...error instanceof SendActivityError ? { "http.response.status_code": error.statusCode } : {}
|
|
5541
|
+
});
|
|
5542
|
+
try {
|
|
5543
|
+
await this.onOutboxError?.(error, await getActivity());
|
|
5544
|
+
} catch (handlerError) {
|
|
5545
|
+
logger.error("An unexpected error occurred in onError handler:\n{error}", {
|
|
5546
|
+
...logData,
|
|
5547
|
+
inbox: inboxHref,
|
|
5548
|
+
error: handlerError
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
if (isPermanentFailure) {
|
|
5552
|
+
logger.warn("Permanent delivery failure for activity {activityId} to {inbox} ({status}); not trying the gateway again.", {
|
|
5553
|
+
...logData,
|
|
5554
|
+
inbox: inboxHref,
|
|
5555
|
+
status: error.statusCode
|
|
5556
|
+
});
|
|
5557
|
+
lastPermanent = {
|
|
5558
|
+
error,
|
|
5559
|
+
inbox
|
|
5560
|
+
};
|
|
5561
|
+
continue;
|
|
5562
|
+
}
|
|
5563
|
+
if (postDecision?.type === "drop" && circuit != null) {
|
|
5564
|
+
await drop(circuit, remoteHost, inbox, postDecision.heldSince);
|
|
5565
|
+
return;
|
|
5566
|
+
} else if (postDecision?.type === "hold") {
|
|
5567
|
+
recordCircuitBreakerHeldSpanEvent(span, remoteHost, postDecision.state);
|
|
5568
|
+
survivors.push({
|
|
5569
|
+
inbox: inboxHref,
|
|
5570
|
+
holdDelay: retryAfter == null ? postDecision.delay : maxDelay(postDecision.delay, retryAfter),
|
|
5571
|
+
circuitHeld: true,
|
|
5572
|
+
postFailure: true,
|
|
5573
|
+
notBefore: retryAt,
|
|
5574
|
+
retryAfter
|
|
5575
|
+
});
|
|
5576
|
+
} else survivors.push({
|
|
5577
|
+
inbox: inboxHref,
|
|
5578
|
+
notBefore: retryAt,
|
|
5579
|
+
retryAfter
|
|
5580
|
+
});
|
|
5581
|
+
logger.warn("Failed to send activity {activityId} to {inbox} (attempt #{attempt}); trying the next gateway if any:\n{error}", {
|
|
5582
|
+
...logData,
|
|
5583
|
+
inbox: inboxHref,
|
|
5584
|
+
error
|
|
5585
|
+
});
|
|
5586
|
+
continue;
|
|
5587
|
+
}
|
|
5588
|
+
if (circuit != null) try {
|
|
5589
|
+
const stateChange = await circuit.recordSuccess(remoteHost);
|
|
5590
|
+
if (stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, stateChange);
|
|
5591
|
+
} catch (error) {
|
|
5592
|
+
circuitLogger.error("Failed to record successful delivery in circuit breaker state; the activity was already delivered:\n{error}", {
|
|
5593
|
+
...logData,
|
|
5594
|
+
remoteHost,
|
|
5595
|
+
error
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
5598
|
+
logger.info("Successfully sent activity {activityId} to {inbox}.", {
|
|
5599
|
+
...logData,
|
|
5600
|
+
inbox: inboxHref
|
|
5601
|
+
});
|
|
5602
|
+
return;
|
|
5603
|
+
}
|
|
5604
|
+
if (lastError != null) span.setStatus({
|
|
5605
|
+
code: SpanStatusCode.ERROR,
|
|
5606
|
+
message: String(lastError)
|
|
5607
|
+
});
|
|
5608
|
+
if (survivors.length < 1) {
|
|
5609
|
+
const { error, inbox } = lastPermanent;
|
|
5610
|
+
this.metrics.recordPermanentFailure(error.inbox, error.statusCode);
|
|
5611
|
+
logger.warn("Permanent delivery failure for activity {activityId} to every gateway of {portableInbox}; not retrying.", {
|
|
5612
|
+
...logData,
|
|
5613
|
+
portableInbox: message.portableInbox
|
|
5614
|
+
});
|
|
5615
|
+
if (this.outboxPermanentFailureHandler != null) {
|
|
5616
|
+
const ctx = this.#createContext(new URL(message.baseUrl), contextData, { documentLoader: this.documentLoaderFactory(this.#getLoaderOptions(message.baseUrl)) });
|
|
5617
|
+
try {
|
|
5618
|
+
await this.outboxPermanentFailureHandler(ctx, {
|
|
5619
|
+
reason: "http",
|
|
5620
|
+
inbox,
|
|
5621
|
+
activity: await getActivity(),
|
|
5622
|
+
error,
|
|
5623
|
+
statusCode: error.statusCode,
|
|
5624
|
+
actorIds: getActorIds()
|
|
5625
|
+
});
|
|
5626
|
+
} catch (handlerError) {
|
|
5627
|
+
logger.error("An unexpected error occurred in outboxPermanentFailureHandler:\n{error}", {
|
|
5628
|
+
...logData,
|
|
5629
|
+
error: handlerError
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
5634
|
+
return;
|
|
5635
|
+
}
|
|
5636
|
+
const now = Temporal.Now.instant();
|
|
5637
|
+
const gatewayNotBefore = {};
|
|
5638
|
+
for (const survivor of survivors) if (survivor.notBefore != null && Temporal.Instant.compare(survivor.notBefore, now) > 0) gatewayNotBefore[survivor.inbox] = survivor.notBefore.toString();
|
|
5639
|
+
const { circuitHeld: _circuitHeld, gatewayNotBefore: _gatewayNotBefore, ...rest } = message;
|
|
5640
|
+
const nextMessage = {
|
|
5641
|
+
...rest,
|
|
5642
|
+
inbox: survivors[0].inbox,
|
|
5643
|
+
gatewayInboxes: survivors.map((s) => s.inbox),
|
|
5644
|
+
...globalThis.Object.keys(gatewayNotBefore).length < 1 ? {} : { gatewayNotBefore }
|
|
5645
|
+
};
|
|
5646
|
+
const { outboxQueue } = this;
|
|
5647
|
+
const policyDelay = this.outboxRetryPolicy({
|
|
5648
|
+
elapsedTime: Temporal.Instant.from(message.started).until(now),
|
|
5649
|
+
attempts: message.attempt
|
|
5650
|
+
});
|
|
5651
|
+
if (outboxQueue != null && survivors.every((s) => s.holdDelay != null) && (policyDelay != null || !survivors.some((s) => s.postFailure))) {
|
|
5652
|
+
let delay = survivors.map((s) => s.holdDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
|
|
5653
|
+
let heldMessage = nextMessage;
|
|
5654
|
+
if (survivors.some((s) => s.circuitHeld)) {
|
|
5655
|
+
const since = heldSince ?? now;
|
|
5656
|
+
if (circuit != null) delay = circuit.capHeldDelay(since, delay);
|
|
5657
|
+
heldMessage = {
|
|
5658
|
+
...nextMessage,
|
|
5659
|
+
circuitHeld: true,
|
|
5660
|
+
circuitHeldSince: since.toString()
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5663
|
+
logger.warn("Failed to send activity {activityId} to {portableInbox}; holding it until one of its gateways can be tried again.", {
|
|
5664
|
+
...logData,
|
|
5665
|
+
portableInbox: message.portableInbox
|
|
5666
|
+
});
|
|
5667
|
+
await outboxQueue.enqueue(heldMessage, {
|
|
5668
|
+
delay: clampNegativeDelay(delay),
|
|
5669
|
+
orderingKey: message.orderingKey
|
|
5670
|
+
});
|
|
5671
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
5672
|
+
role: "outbox",
|
|
5673
|
+
queue: outboxQueue,
|
|
5674
|
+
activityType: heldMessage.activityType
|
|
5675
|
+
}, heldMessage.attempt);
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
const hasRetryAfter = survivors.some((s) => s.retryAfter != null);
|
|
5679
|
+
if (outboxQueue?.nativeRetrial && !hasRetryAfter && survivors.length === message.gatewayInboxes.length && globalThis.Object.keys(gatewayNotBefore).length < 1) {
|
|
5680
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox}; backend will handle retry:\n{error}", {
|
|
5681
|
+
...logData,
|
|
5682
|
+
portableInbox: message.portableInbox,
|
|
5683
|
+
error: lastError
|
|
5684
|
+
});
|
|
5685
|
+
throw lastError;
|
|
5686
|
+
}
|
|
5687
|
+
if (policyDelay == null || outboxQueue == null) {
|
|
5688
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} after {attempt} attempts; giving up:\n{error}", {
|
|
5689
|
+
...logData,
|
|
5690
|
+
portableInbox: message.portableInbox,
|
|
5691
|
+
error: lastError
|
|
5692
|
+
});
|
|
5693
|
+
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
const delay = survivors.map((s) => s.holdDelay ?? s.retryAfter ?? policyDelay).reduce((a, b) => Temporal.Duration.compare(a, b) <= 0 ? a : b);
|
|
5697
|
+
const retryMessage = {
|
|
5698
|
+
...nextMessage,
|
|
5699
|
+
attempt: message.attempt + 1
|
|
5700
|
+
};
|
|
5701
|
+
logger.error("Failed to send activity {activityId} to every gateway of {portableInbox} (attempt #{attempt}); retry...:\n{error}", {
|
|
5702
|
+
...logData,
|
|
5703
|
+
portableInbox: message.portableInbox,
|
|
5704
|
+
error: lastError
|
|
5705
|
+
});
|
|
5706
|
+
await outboxQueue.enqueue(retryMessage, {
|
|
5707
|
+
delay: clampNegativeDelay(delay),
|
|
5708
|
+
orderingKey: message.orderingKey
|
|
5709
|
+
});
|
|
5710
|
+
this.metrics.recordQueueTaskEnqueued({
|
|
5711
|
+
role: "outbox",
|
|
5712
|
+
queue: outboxQueue,
|
|
5713
|
+
activityType: retryMessage.activityType
|
|
5714
|
+
}, retryMessage.attempt);
|
|
5715
|
+
recordOutboxActivity(this.meterProvider, "retried", retryMessage.activityType);
|
|
5716
|
+
}
|
|
3524
5717
|
async #listenInboxMessage(ctxData, message, span, onActivityType) {
|
|
3525
5718
|
const logger = getLogger([
|
|
3526
5719
|
"fedify",
|
|
@@ -3529,8 +5722,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3529
5722
|
]);
|
|
3530
5723
|
const baseUrl = new URL(message.baseUrl);
|
|
3531
5724
|
let context = this.#createContext(baseUrl, ctxData);
|
|
3532
|
-
if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
3533
|
-
else if (this.sharedInboxKeyDispatcher != null) {
|
|
5725
|
+
if (message.identifier != null && this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
5726
|
+
else if (message.identifier == null && this.sharedInboxKeyDispatcher != null) {
|
|
3534
5727
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
3535
5728
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
3536
5729
|
}
|
|
@@ -3862,7 +6055,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3862
6055
|
...ctxOptions,
|
|
3863
6056
|
request,
|
|
3864
6057
|
invokedFromActorDispatcher: opts.invokedFromActorDispatcher,
|
|
3865
|
-
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher
|
|
6058
|
+
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher,
|
|
6059
|
+
portableRequest: opts.portableRequest
|
|
3866
6060
|
});
|
|
3867
6061
|
}
|
|
3868
6062
|
#getLoaderOptions(origin) {
|
|
@@ -3887,36 +6081,33 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3887
6081
|
else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
|
|
3888
6082
|
const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
|
|
3889
6083
|
const activityId = activity.id.href;
|
|
3890
|
-
let hasProof = false;
|
|
3891
|
-
let proofCreated = false;
|
|
3892
6084
|
let rsaKey = null;
|
|
6085
|
+
const ldSignable = !hasPortableActor(activity);
|
|
3893
6086
|
for (const { keyId, privateKey } of keys) {
|
|
3894
6087
|
validateCryptoKey(privateKey, "private");
|
|
3895
|
-
if (rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") rsaKey = {
|
|
6088
|
+
if (ldSignable && rsaKey == null && privateKey.algorithm.name === "RSASSA-PKCS1-v1_5" && !isPortableKeyId(keyId)) rsaKey = {
|
|
3896
6089
|
keyId,
|
|
3897
6090
|
privateKey
|
|
3898
6091
|
};
|
|
3899
6092
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
proofCreated = true;
|
|
3912
|
-
}
|
|
3913
|
-
}
|
|
3914
|
-
let jsonLd = await activity.toJsonLd({
|
|
6093
|
+
const signed = await signOutgoingActivity(activity, keys.map(({ keyId, privateKey }) => ({
|
|
6094
|
+
verificationMethod: keyId,
|
|
6095
|
+
privateKey
|
|
6096
|
+
})), {
|
|
6097
|
+
contextLoader,
|
|
6098
|
+
tracerProvider: this.tracerProvider,
|
|
6099
|
+
appendToExistingProofs: false
|
|
6100
|
+
});
|
|
6101
|
+
activity = signed.activity;
|
|
6102
|
+
const { hasProof, proofCreated } = signed;
|
|
6103
|
+
let jsonLd = !proofCreated && options.activityJsonLd != null ? options.activityJsonLd : await activity.toJsonLd({
|
|
3915
6104
|
format: "compact",
|
|
3916
6105
|
contextLoader
|
|
3917
6106
|
});
|
|
3918
|
-
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader);
|
|
3919
|
-
|
|
6107
|
+
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader, { preserveNestedSecuredDocuments: true });
|
|
6108
|
+
assertSupportedCompoundProofShape(jsonLd, activityId);
|
|
6109
|
+
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 });
|
|
6110
|
+
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.", {
|
|
3920
6111
|
activityId,
|
|
3921
6112
|
keys: keys.map((pair) => ({
|
|
3922
6113
|
keyId: pair.keyId.href,
|
|
@@ -3952,15 +6143,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3952
6143
|
activity: jsonLd
|
|
3953
6144
|
});
|
|
3954
6145
|
const promises = [];
|
|
3955
|
-
for (const inbox in inboxes) promises.push(
|
|
6146
|
+
for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
|
|
6147
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
3956
6148
|
keys,
|
|
3957
6149
|
activity: jsonLd,
|
|
3958
6150
|
activityId: activity.id?.href,
|
|
3959
6151
|
activityType: getTypeId(activity).href,
|
|
3960
|
-
inbox: new URL(inbox),
|
|
3961
6152
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
3962
6153
|
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
|
3963
|
-
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
6154
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
3964
6155
|
meterProvider: this.meterProvider,
|
|
3965
6156
|
tracerProvider: this.tracerProvider
|
|
3966
6157
|
}));
|
|
@@ -3984,8 +6175,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3984
6175
|
propagation.inject(context.active(), carrier);
|
|
3985
6176
|
const messages = [];
|
|
3986
6177
|
for (const inbox in inboxes) {
|
|
3987
|
-
const
|
|
3988
|
-
const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${
|
|
6178
|
+
const { portableInbox, gatewayInboxes } = inboxes[inbox];
|
|
6179
|
+
const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`;
|
|
3989
6180
|
const message = {
|
|
3990
6181
|
type: "outbox",
|
|
3991
6182
|
id: crypto.randomUUID(),
|
|
@@ -4001,6 +6192,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4001
6192
|
attempt: 0,
|
|
4002
6193
|
headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
|
|
4003
6194
|
orderingKey: messageOrderingKey,
|
|
6195
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
4004
6196
|
traceContext: carrier
|
|
4005
6197
|
};
|
|
4006
6198
|
messages.push({
|
|
@@ -4051,7 +6243,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4051
6243
|
return await withContext({
|
|
4052
6244
|
traceId: spanCtx.traceId,
|
|
4053
6245
|
spanId: spanCtx.spanId
|
|
4054
|
-
}, async () => {
|
|
6246
|
+
}, () => this.#observeInboxFetch(metricState, async () => {
|
|
4055
6247
|
const logger = getLogger([
|
|
4056
6248
|
"fedify",
|
|
4057
6249
|
"federation",
|
|
@@ -4066,8 +6258,14 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4066
6258
|
tracer,
|
|
4067
6259
|
metricState
|
|
4068
6260
|
});
|
|
4069
|
-
if (
|
|
6261
|
+
if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
|
|
6262
|
+
if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
|
|
4070
6263
|
} catch (error) {
|
|
6264
|
+
metricState.inboxCompletion?.observation.project(span, {
|
|
6265
|
+
type: "exception",
|
|
6266
|
+
stage: metricState.inboxCompletion.observation.stage,
|
|
6267
|
+
error
|
|
6268
|
+
});
|
|
4071
6269
|
this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
|
|
4072
6270
|
span.setStatus({
|
|
4073
6271
|
code: SpanStatusCode.ERROR,
|
|
@@ -4106,7 +6304,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4106
6304
|
else if (response.status >= 400) logger.warn(logTpl, values);
|
|
4107
6305
|
else logger.info(logTpl, values);
|
|
4108
6306
|
return response;
|
|
4109
|
-
});
|
|
6307
|
+
}));
|
|
4110
6308
|
});
|
|
4111
6309
|
});
|
|
4112
6310
|
}
|
|
@@ -4117,14 +6315,48 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4117
6315
|
const url = new URL(request.url);
|
|
4118
6316
|
const route = this.router.route(url.pathname);
|
|
4119
6317
|
if (route == null) {
|
|
6318
|
+
const media = this.hashlinkMediaDispatcher == null ? null : parseHashlinkGatewayRequest(url);
|
|
6319
|
+
if (media != null) return await this.#fetchHashlinkMedia(request, media, {
|
|
6320
|
+
onNotFound,
|
|
6321
|
+
contextData,
|
|
6322
|
+
span,
|
|
6323
|
+
metricState
|
|
6324
|
+
});
|
|
6325
|
+
if (request.method === "POST") {
|
|
6326
|
+
const portable = parsePortableGatewayRequest(url);
|
|
6327
|
+
if (portable != null) return await this.#fetchPortableInbox(request, portable, {
|
|
6328
|
+
onNotFound,
|
|
6329
|
+
contextData,
|
|
6330
|
+
span,
|
|
6331
|
+
metricState
|
|
6332
|
+
});
|
|
6333
|
+
}
|
|
6334
|
+
const portable = request.method === "GET" || request.method === "HEAD" ? parsePortableGatewayRequest(url) : null;
|
|
6335
|
+
if (portable != null) return await discardBodyForHead(request, await this.#fetchPortableObject(request, portable, {
|
|
6336
|
+
onNotFound,
|
|
6337
|
+
onNotAcceptable,
|
|
6338
|
+
onUnauthorized,
|
|
6339
|
+
contextData,
|
|
6340
|
+
span,
|
|
6341
|
+
metricState
|
|
6342
|
+
}));
|
|
4120
6343
|
metricState.endpoint = "not_found";
|
|
4121
6344
|
return await onNotFound(request);
|
|
4122
6345
|
}
|
|
4123
6346
|
metricState.routeTemplate = route.template;
|
|
4124
6347
|
metricState.endpoint = getEndpointCategory(route.name);
|
|
4125
6348
|
span.updateName(`${request.method} ${route.template}`);
|
|
4126
|
-
let context = this.#createContext(request, contextData);
|
|
4127
6349
|
const routeName = route.name.replace(/:.*$/, "");
|
|
6350
|
+
let context;
|
|
6351
|
+
try {
|
|
6352
|
+
context = this.#createContext(request, contextData);
|
|
6353
|
+
} catch (error) {
|
|
6354
|
+
if (request.method !== "POST" || routeName !== "inbox" && routeName !== "sharedInbox") throw error;
|
|
6355
|
+
return await this.#reportInboxPreparationError(request, contextData, {
|
|
6356
|
+
kind: routeName === "inbox" ? "personal" : "shared",
|
|
6357
|
+
recipient: route.values.identifier ?? null
|
|
6358
|
+
}, error, span, metricState);
|
|
6359
|
+
}
|
|
4128
6360
|
switch (routeName) {
|
|
4129
6361
|
case "webfinger": return await handleWebFinger(request, {
|
|
4130
6362
|
context,
|
|
@@ -4183,6 +6415,28 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4183
6415
|
});
|
|
4184
6416
|
return response;
|
|
4185
6417
|
}
|
|
6418
|
+
if (request.method === "POST" && (routeName === "inbox" || routeName === "sharedInbox")) {
|
|
6419
|
+
const recipient = route.values.identifier ?? null;
|
|
6420
|
+
const observation = new InboxObservation({
|
|
6421
|
+
kind: routeName === "inbox" ? "personal" : "shared",
|
|
6422
|
+
recipient
|
|
6423
|
+
});
|
|
6424
|
+
this.#setInboxCompletion(metricState, observation, () => context);
|
|
6425
|
+
return await observation.run(async () => {
|
|
6426
|
+
if (routeName === "inbox") context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: recipient }) });
|
|
6427
|
+
else if (this.sharedInboxKeyDispatcher != null) {
|
|
6428
|
+
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
6429
|
+
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
6430
|
+
}
|
|
6431
|
+
return await this.#handleInbox(request, {
|
|
6432
|
+
recipient,
|
|
6433
|
+
context,
|
|
6434
|
+
contextData,
|
|
6435
|
+
onNotFound,
|
|
6436
|
+
observation
|
|
6437
|
+
});
|
|
6438
|
+
}, span);
|
|
6439
|
+
}
|
|
4186
6440
|
switch (routeName) {
|
|
4187
6441
|
case "actor":
|
|
4188
6442
|
case "actorAlias": {
|
|
@@ -4258,35 +6512,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4258
6512
|
onUnauthorized,
|
|
4259
6513
|
onNotFound
|
|
4260
6514
|
});
|
|
4261
|
-
|
|
4262
|
-
case "sharedInbox":
|
|
4263
|
-
if (
|
|
6515
|
+
throw new Error("Unreachable inbox delivery.");
|
|
6516
|
+
case "sharedInbox":
|
|
6517
|
+
if (this.sharedInboxKeyDispatcher != null) {
|
|
4264
6518
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
4265
6519
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
4266
|
-
}
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
context,
|
|
4273
|
-
inboxContextFactory,
|
|
4274
|
-
kv: this.kv,
|
|
4275
|
-
kvPrefixes: this.kvPrefixes,
|
|
4276
|
-
queue: this.inboxQueue,
|
|
4277
|
-
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
4278
|
-
inboxListeners: this.inboxListeners,
|
|
4279
|
-
inboxErrorHandler: this.inboxErrorHandler,
|
|
4280
|
-
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
4281
|
-
onNotFound,
|
|
4282
|
-
signatureTimeWindow: this.signatureTimeWindow,
|
|
4283
|
-
skipSignatureVerification: this.skipSignatureVerification,
|
|
4284
|
-
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
4285
|
-
meterProvider: this.meterProvider,
|
|
4286
|
-
tracerProvider: this.tracerProvider,
|
|
4287
|
-
idempotencyStrategy: this.idempotencyStrategy
|
|
6520
|
+
}
|
|
6521
|
+
return await this.#handleInbox(request, {
|
|
6522
|
+
recipient: route.values.identifier ?? null,
|
|
6523
|
+
context,
|
|
6524
|
+
contextData,
|
|
6525
|
+
onNotFound
|
|
4288
6526
|
});
|
|
4289
|
-
}
|
|
4290
6527
|
case "following": return await handleCollection(request, {
|
|
4291
6528
|
name: "following",
|
|
4292
6529
|
identifier: route.values.identifier,
|
|
@@ -4299,12 +6536,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4299
6536
|
onNotFound
|
|
4300
6537
|
});
|
|
4301
6538
|
case "followers": {
|
|
4302
|
-
|
|
4303
|
-
if (baseUrl != null) try {
|
|
4304
|
-
baseUrl = `${new URL(baseUrl).origin}/`;
|
|
4305
|
-
} catch {
|
|
4306
|
-
baseUrl = null;
|
|
4307
|
-
}
|
|
6539
|
+
const baseUrl = getFollowersBaseUrl(url);
|
|
4308
6540
|
return await handleCollection(request, {
|
|
4309
6541
|
name: "followers",
|
|
4310
6542
|
identifier: route.values.identifier,
|
|
@@ -4315,7 +6547,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4315
6547
|
},
|
|
4316
6548
|
context,
|
|
4317
6549
|
filter: baseUrl != null ? new URL(baseUrl) : void 0,
|
|
4318
|
-
filterPredicate: baseUrl != null ? (
|
|
6550
|
+
filterPredicate: baseUrl != null ? getFollowersFilterPredicate(baseUrl) : void 0,
|
|
4319
6551
|
collectionCallbacks: this.followersCallbacks,
|
|
4320
6552
|
tracerProvider: this.tracerProvider,
|
|
4321
6553
|
meterProvider: this._meterProvider,
|
|
@@ -4357,10 +6589,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4357
6589
|
onNotFound
|
|
4358
6590
|
});
|
|
4359
6591
|
case "collection": {
|
|
4360
|
-
const name = route.name
|
|
6592
|
+
const name = this.getCollectionName(route.name);
|
|
4361
6593
|
const callbacks = this.collectionCallbacks[name];
|
|
4362
6594
|
return await handleCustomCollection(request, {
|
|
4363
|
-
name,
|
|
6595
|
+
name: String(name),
|
|
4364
6596
|
context,
|
|
4365
6597
|
values: route.values,
|
|
4366
6598
|
collectionCallbacks: callbacks,
|
|
@@ -4371,10 +6603,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4371
6603
|
});
|
|
4372
6604
|
}
|
|
4373
6605
|
case "orderedCollection": {
|
|
4374
|
-
const name = route.name
|
|
6606
|
+
const name = this.getCollectionName(route.name);
|
|
4375
6607
|
const callbacks = this.collectionCallbacks[name];
|
|
4376
6608
|
return await handleOrderedCollection(request, {
|
|
4377
|
-
name,
|
|
6609
|
+
name: String(name),
|
|
4378
6610
|
context,
|
|
4379
6611
|
values: route.values,
|
|
4380
6612
|
collectionCallbacks: callbacks,
|
|
@@ -4391,7 +6623,499 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4391
6623
|
}
|
|
4392
6624
|
}
|
|
4393
6625
|
}
|
|
6626
|
+
async #fetchPortableObject(request, portable, { onNotFound, onNotAcceptable, onUnauthorized, contextData, span, metricState }) {
|
|
6627
|
+
if (portable.type === "malformed") {
|
|
6628
|
+
metricState.endpoint = "not_found";
|
|
6629
|
+
getLogger([
|
|
6630
|
+
"fedify",
|
|
6631
|
+
"federation",
|
|
6632
|
+
"object"
|
|
6633
|
+
]).debug("Malformed FEP-ef61 gateway request {url}: {error}", {
|
|
6634
|
+
url: request.url,
|
|
6635
|
+
error: portable.error
|
|
6636
|
+
});
|
|
6637
|
+
return new Response(request.method === "HEAD" ? null : "Malformed portable object ID.", {
|
|
6638
|
+
status: 400,
|
|
6639
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6640
|
+
});
|
|
6641
|
+
}
|
|
6642
|
+
const route = this.router.route(portable.path);
|
|
6643
|
+
const isActor = route != null && (route.name === "actor" || route.name.startsWith("actorAlias:"));
|
|
6644
|
+
const collectionRoute = route == null ? void 0 : getCollectionMetricRoute(route.name.replace(/:.*$/, ""));
|
|
6645
|
+
if (route == null || !isActor && !route.name.startsWith("object:") && collectionRoute == null) {
|
|
6646
|
+
metricState.endpoint = "not_found";
|
|
6647
|
+
return await onNotFound(request);
|
|
6648
|
+
}
|
|
6649
|
+
metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
|
|
6650
|
+
metricState.endpoint = getEndpointCategory(route.name);
|
|
6651
|
+
span.updateName(`${request.method} ${metricState.routeTemplate}`);
|
|
6652
|
+
if (!acceptsJsonLd(request)) {
|
|
6653
|
+
metricState.endpoint = "not_acceptable";
|
|
6654
|
+
const response = await onNotAcceptable(request);
|
|
6655
|
+
if (collectionRoute != null) recordCollectionRequest(this._meterProvider, {
|
|
6656
|
+
...collectionRoute,
|
|
6657
|
+
page: new URL(request.url).searchParams.get("cursor") != null,
|
|
6658
|
+
result: "not_acceptable",
|
|
6659
|
+
statusCode: response.status
|
|
6660
|
+
});
|
|
6661
|
+
return response;
|
|
6662
|
+
}
|
|
6663
|
+
if (collectionRoute != null) return await this.#fetchPortableCollection(request, portable, route, {
|
|
6664
|
+
onNotFound,
|
|
6665
|
+
onUnauthorized,
|
|
6666
|
+
contextData
|
|
6667
|
+
});
|
|
6668
|
+
if (isActor) {
|
|
6669
|
+
const identifier = route.name.startsWith("actorAlias:") ? route.name.substring(ACTOR_ALIAS_PREFIX.length) : route.values.identifier;
|
|
6670
|
+
const context = this.#createContext(request, contextData, {
|
|
6671
|
+
invokedFromActorDispatcher: { identifier },
|
|
6672
|
+
portableRequest: portable.portableRequest
|
|
6673
|
+
});
|
|
6674
|
+
const actorDispatcher = this.actorCallbacks?.dispatcher;
|
|
6675
|
+
const authorizePredicate = this.actorCallbacks?.authorizePredicate;
|
|
6676
|
+
return await handlePortableObject(request, {
|
|
6677
|
+
values: { identifier },
|
|
6678
|
+
context,
|
|
6679
|
+
objectDispatcher: actorDispatcher == null ? void 0 : (ctx) => actorDispatcher(ctx, identifier),
|
|
6680
|
+
authorizePredicate: authorizePredicate == null ? void 0 : (ctx) => authorizePredicate(ctx, identifier),
|
|
6681
|
+
canonicalId: portable.canonicalId,
|
|
6682
|
+
kind: "actor",
|
|
6683
|
+
onUnauthorized,
|
|
6684
|
+
onNotFound
|
|
6685
|
+
});
|
|
6686
|
+
}
|
|
6687
|
+
const typeId = route.name.replace(/^object:/, "");
|
|
6688
|
+
const callbacks = this.objectCallbacks[typeId];
|
|
6689
|
+
const cls = this.objectTypeIds[typeId];
|
|
6690
|
+
const context = this.#createContext(request, contextData, {
|
|
6691
|
+
invokedFromObjectDispatcher: {
|
|
6692
|
+
cls,
|
|
6693
|
+
values: route.values
|
|
6694
|
+
},
|
|
6695
|
+
portableRequest: portable.portableRequest
|
|
6696
|
+
});
|
|
6697
|
+
return await handlePortableObject(request, {
|
|
6698
|
+
values: route.values,
|
|
6699
|
+
context,
|
|
6700
|
+
objectDispatcher: callbacks?.dispatcher,
|
|
6701
|
+
authorizePredicate: callbacks?.authorizePredicate,
|
|
6702
|
+
canonicalId: portable.canonicalId,
|
|
6703
|
+
kind: "object",
|
|
6704
|
+
onUnauthorized,
|
|
6705
|
+
onNotFound
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6708
|
+
async #fetchPortableCollection(request, portable, route, { onNotFound, onUnauthorized, contextData }) {
|
|
6709
|
+
const logger = getLogger([
|
|
6710
|
+
"fedify",
|
|
6711
|
+
"federation",
|
|
6712
|
+
"collection"
|
|
6713
|
+
]);
|
|
6714
|
+
const url = new URL(request.url);
|
|
6715
|
+
const { authority } = portable.portableRequest;
|
|
6716
|
+
const ownerContext = this.#createContext(request, contextData);
|
|
6717
|
+
const context = this.#createContext(request, contextData, { portableRequest: portable.portableRequest });
|
|
6718
|
+
const reject = async (reason, identifier) => {
|
|
6719
|
+
logger.debug("Not serving the portable collection {collection} of the actor {identifier}: {reason}.", {
|
|
6720
|
+
collection: portable.canonicalId,
|
|
6721
|
+
identifier,
|
|
6722
|
+
reason
|
|
6723
|
+
});
|
|
6724
|
+
return await onNotFound(request);
|
|
6725
|
+
};
|
|
6726
|
+
const routeName = route.name.replace(/:.*$/, "");
|
|
6727
|
+
if (routeName === "collection" || routeName === "orderedCollection") {
|
|
6728
|
+
const name = route.name.replace(/^(?:ordered)?[cC]ollection:/, "");
|
|
6729
|
+
const callbacks = this.collectionCallbacks[name];
|
|
6730
|
+
if (callbacks == null) return await onNotFound(request);
|
|
6731
|
+
const mapper = callbacks.portableOwnerMapper;
|
|
6732
|
+
if (mapper == null) return await reject("noPortableOwnerMapper");
|
|
6733
|
+
const identifier = await mapper(ownerContext, route.values);
|
|
6734
|
+
if (identifier == null) return await reject("notPortable");
|
|
6735
|
+
const resolution = resolvePortableCollectionOwner(await this.actorCallbacks?.dispatcher?.(ownerContext, identifier), {
|
|
6736
|
+
authority,
|
|
6737
|
+
portableId: portable.portableRequest.id
|
|
6738
|
+
});
|
|
6739
|
+
if (resolution.status === "rejected") return await reject(resolution.reason, identifier);
|
|
6740
|
+
const view = buildPortableCollectionView(resolution.collectionId, url);
|
|
6741
|
+
return await (routeName === "collection" ? handleCustomCollection : handleOrderedCollection)(request, {
|
|
6742
|
+
name,
|
|
6743
|
+
context,
|
|
6744
|
+
values: route.values,
|
|
6745
|
+
collectionCallbacks: callbacks,
|
|
6746
|
+
portable: {
|
|
6747
|
+
id: view,
|
|
6748
|
+
view,
|
|
6749
|
+
attribution: resolution.ownerId
|
|
6750
|
+
},
|
|
6751
|
+
tracerProvider: this.tracerProvider,
|
|
6752
|
+
meterProvider: this._meterProvider,
|
|
6753
|
+
onUnauthorized,
|
|
6754
|
+
onNotFound
|
|
6755
|
+
});
|
|
6756
|
+
}
|
|
6757
|
+
const identifier = route.values.identifier;
|
|
6758
|
+
const serve = async (name, callbacks, property, filter) => {
|
|
6759
|
+
if (callbacks == null) return await onNotFound(request);
|
|
6760
|
+
const resolution = resolvePortableCollectionOwner(await this.actorCallbacks?.dispatcher?.(ownerContext, identifier), {
|
|
6761
|
+
authority,
|
|
6762
|
+
canonicalId: portable.canonicalId,
|
|
6763
|
+
property
|
|
6764
|
+
});
|
|
6765
|
+
if (resolution.status === "rejected") return await reject(resolution.reason, identifier);
|
|
6766
|
+
const id = new URL(resolution.collectionId);
|
|
6767
|
+
if (filter != null) id.searchParams.set(...filter.parameter);
|
|
6768
|
+
const view = buildPortableCollectionView(resolution.collectionId, url);
|
|
6769
|
+
if (routeName === "followers") {
|
|
6770
|
+
view.searchParams.delete("base-url");
|
|
6771
|
+
if (filter != null) view.searchParams.set(...filter.parameter);
|
|
6772
|
+
}
|
|
6773
|
+
return await handleCollection(request, {
|
|
6774
|
+
name,
|
|
6775
|
+
identifier,
|
|
6776
|
+
uriGetter: () => id,
|
|
6777
|
+
context,
|
|
6778
|
+
filter: filter?.value,
|
|
6779
|
+
filterPredicate: filter?.predicate,
|
|
6780
|
+
collectionCallbacks: callbacks,
|
|
6781
|
+
portable: {
|
|
6782
|
+
id,
|
|
6783
|
+
view,
|
|
6784
|
+
attribution: resolution.ownerId
|
|
6785
|
+
},
|
|
6786
|
+
tracerProvider: this.tracerProvider,
|
|
6787
|
+
meterProvider: this._meterProvider,
|
|
6788
|
+
onUnauthorized,
|
|
6789
|
+
onNotFound
|
|
6790
|
+
});
|
|
6791
|
+
};
|
|
6792
|
+
switch (routeName) {
|
|
6793
|
+
case "outbox": return await serve("outbox", this.outboxCallbacks, (a) => a.outboxId);
|
|
6794
|
+
case "inbox": return await serve("inbox", this.inboxCallbacks, (a) => a.inboxId);
|
|
6795
|
+
case "following": return await serve("following", this.followingCallbacks, (a) => a.followingId);
|
|
6796
|
+
case "followers": {
|
|
6797
|
+
const baseUrl = getFollowersBaseUrl(url);
|
|
6798
|
+
return await serve("followers", this.followersCallbacks, (a) => a.followersId, baseUrl == null ? void 0 : {
|
|
6799
|
+
value: new URL(baseUrl),
|
|
6800
|
+
predicate: getFollowersFilterPredicate(baseUrl),
|
|
6801
|
+
parameter: ["base-url", baseUrl]
|
|
6802
|
+
});
|
|
6803
|
+
}
|
|
6804
|
+
case "liked": return await serve("liked", this.likedCallbacks, (a) => a.likedId);
|
|
6805
|
+
case "featured": return await serve("featured", this.featuredCallbacks, (a) => a.featuredId);
|
|
6806
|
+
case "featuredTags": return await serve("featured tags", this.featuredTagsCallbacks, (a) => a.featuredTagsId);
|
|
6807
|
+
}
|
|
6808
|
+
return await onNotFound(request);
|
|
6809
|
+
}
|
|
6810
|
+
#setInboxCompletion(metricState, observation, getContext) {
|
|
6811
|
+
const handler = this.inboxRequestFinishedHandler;
|
|
6812
|
+
metricState.inboxCompletion = {
|
|
6813
|
+
observation,
|
|
6814
|
+
finish: (outcome) => observation.finish(getContext(), handler, outcome)
|
|
6815
|
+
};
|
|
6816
|
+
}
|
|
6817
|
+
async #observeInboxFetch(metricState, operation) {
|
|
6818
|
+
let response;
|
|
6819
|
+
try {
|
|
6820
|
+
response = await operation();
|
|
6821
|
+
} catch (error) {
|
|
6822
|
+
const completion = metricState.inboxCompletion;
|
|
6823
|
+
if (completion != null) await completion.finish({
|
|
6824
|
+
type: "exception",
|
|
6825
|
+
stage: completion.observation.stage,
|
|
6826
|
+
error
|
|
6827
|
+
});
|
|
6828
|
+
throw error;
|
|
6829
|
+
}
|
|
6830
|
+
const completion = metricState.inboxCompletion;
|
|
6831
|
+
if (completion != null) await completion.finish({
|
|
6832
|
+
type: "response",
|
|
6833
|
+
status: response.status,
|
|
6834
|
+
...completion.observation.result
|
|
6835
|
+
});
|
|
6836
|
+
return response;
|
|
6837
|
+
}
|
|
6838
|
+
async #reportInboxPreparationError(request, contextData, inbox, error, span, metricState) {
|
|
6839
|
+
const unavailableLoader = () => Promise.reject(error);
|
|
6840
|
+
const context = new RequestContextImpl({
|
|
6841
|
+
url: new URL(request.url),
|
|
6842
|
+
request,
|
|
6843
|
+
federation: this,
|
|
6844
|
+
data: contextData,
|
|
6845
|
+
documentLoader: unavailableLoader,
|
|
6846
|
+
contextLoader: unavailableLoader
|
|
6847
|
+
});
|
|
6848
|
+
const observation = new InboxObservation(inbox);
|
|
6849
|
+
this.#setInboxCompletion(metricState, observation, () => context);
|
|
6850
|
+
return await observation.run(() => Promise.reject(error), span);
|
|
6851
|
+
}
|
|
6852
|
+
async #handleInbox(request, { recipient, context, contextData, onNotFound, portableInbox, observation }) {
|
|
6853
|
+
if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
|
|
6854
|
+
const inboxContextFactory = context.toInboxContext.bind(context);
|
|
6855
|
+
inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
|
|
6856
|
+
return await handleInbox(request, {
|
|
6857
|
+
recipient,
|
|
6858
|
+
context,
|
|
6859
|
+
inboxContextFactory,
|
|
6860
|
+
kv: this.kv,
|
|
6861
|
+
kvPrefixes: this.kvPrefixes,
|
|
6862
|
+
publicKeyTtl: this.publicKeyTtl,
|
|
6863
|
+
queue: this.inboxQueue,
|
|
6864
|
+
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
6865
|
+
inboxListeners: this.inboxListeners,
|
|
6866
|
+
inboxErrorHandler: this.inboxErrorHandler,
|
|
6867
|
+
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
6868
|
+
onNotFound,
|
|
6869
|
+
signatureTimeWindow: this.signatureTimeWindow,
|
|
6870
|
+
maxHttpSignatures: this.maxHttpSignatures,
|
|
6871
|
+
skipSignatureVerification: this.skipSignatureVerification,
|
|
6872
|
+
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
6873
|
+
meterProvider: this.meterProvider,
|
|
6874
|
+
tracerProvider: this.tracerProvider,
|
|
6875
|
+
idempotencyStrategy: this.idempotencyStrategy,
|
|
6876
|
+
portableInbox,
|
|
6877
|
+
observation
|
|
6878
|
+
});
|
|
6879
|
+
}
|
|
6880
|
+
async #fetchPortableInbox(request, portable, { onNotFound, contextData, span, metricState }) {
|
|
6881
|
+
const logger = getLogger([
|
|
6882
|
+
"fedify",
|
|
6883
|
+
"federation",
|
|
6884
|
+
"inbox"
|
|
6885
|
+
]);
|
|
6886
|
+
if (portable.type === "malformed") {
|
|
6887
|
+
metricState.endpoint = "not_found";
|
|
6888
|
+
logger.debug("Malformed FEP-ef61 gateway inbox request {url}: {error}", {
|
|
6889
|
+
url: request.url,
|
|
6890
|
+
error: portable.error
|
|
6891
|
+
});
|
|
6892
|
+
return new Response("Malformed portable inbox ID.", {
|
|
6893
|
+
status: 400,
|
|
6894
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
6895
|
+
});
|
|
6896
|
+
}
|
|
6897
|
+
const route = this.router.route(portable.path);
|
|
6898
|
+
if (route == null || route.name !== "inbox") {
|
|
6899
|
+
metricState.endpoint = "not_found";
|
|
6900
|
+
return await onNotFound(request);
|
|
6901
|
+
}
|
|
6902
|
+
metricState.routeTemplate = PORTABLE_GATEWAY_ROUTE_PREFIX + route.template;
|
|
6903
|
+
metricState.endpoint = "inbox";
|
|
6904
|
+
span.updateName(`${request.method} ${metricState.routeTemplate}`);
|
|
6905
|
+
const identifier = route.values.identifier;
|
|
6906
|
+
let context;
|
|
6907
|
+
try {
|
|
6908
|
+
context = this.#createContext(request, contextData);
|
|
6909
|
+
} catch (error) {
|
|
6910
|
+
return await this.#reportInboxPreparationError(request, contextData, {
|
|
6911
|
+
kind: "portable",
|
|
6912
|
+
recipient: identifier
|
|
6913
|
+
}, error, span, metricState);
|
|
6914
|
+
}
|
|
6915
|
+
const observation = new InboxObservation({
|
|
6916
|
+
kind: "portable",
|
|
6917
|
+
recipient: identifier
|
|
6918
|
+
});
|
|
6919
|
+
this.#setInboxCompletion(metricState, observation, () => context);
|
|
6920
|
+
return await observation.run(async () => {
|
|
6921
|
+
const actor = this.actorCallbacks?.dispatcher == null ? null : await this.actorCallbacks.dispatcher(context, identifier);
|
|
6922
|
+
const localOrigin = context.canonicalOrigin;
|
|
6923
|
+
const resolution = resolvePortableInboxRecipient(actor == null || actor instanceof Tombstone ? null : actor, {
|
|
6924
|
+
authority: portable.portableRequest.authority,
|
|
6925
|
+
canonicalInboxId: portable.canonicalId,
|
|
6926
|
+
localOrigin
|
|
6927
|
+
});
|
|
6928
|
+
if (resolution.status === "rejected") {
|
|
6929
|
+
logger.debug("Not accepting a delivery to the portable inbox {inbox} on behalf of the actor {identifier}: {reason}.", {
|
|
6930
|
+
inbox: portable.canonicalId,
|
|
6931
|
+
identifier,
|
|
6932
|
+
reason: resolution.reason
|
|
6933
|
+
});
|
|
6934
|
+
return await onNotFound(request);
|
|
6935
|
+
}
|
|
6936
|
+
const { recipient } = resolution;
|
|
6937
|
+
if (this.actorCallbacks?.keyPairsDispatcher != null) context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier }) });
|
|
6938
|
+
const excludedOrigins = [localOrigin, new URL(request.url).origin];
|
|
6939
|
+
return await this.#handleInbox(request, {
|
|
6940
|
+
recipient: identifier,
|
|
6941
|
+
observation,
|
|
6942
|
+
context,
|
|
6943
|
+
contextData,
|
|
6944
|
+
onNotFound,
|
|
6945
|
+
portableInbox: {
|
|
6946
|
+
recipient,
|
|
6947
|
+
forward: async (activity, activityId, activityType, signatures) => {
|
|
6948
|
+
if (this.portableInboxForwarding.maxTargets < 1) return;
|
|
6949
|
+
if (this.kv.cas == null) {
|
|
6950
|
+
if (!this.#portableInboxForwardingWarned) {
|
|
6951
|
+
this.#portableInboxForwardingWarned = true;
|
|
6952
|
+
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.");
|
|
6953
|
+
}
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
await forwardPortableInboxActivity({
|
|
6957
|
+
recipient,
|
|
6958
|
+
activity,
|
|
6959
|
+
activityId,
|
|
6960
|
+
activityType,
|
|
6961
|
+
excludedOrigins,
|
|
6962
|
+
baseUrl: context.origin,
|
|
6963
|
+
kv: this.kv,
|
|
6964
|
+
kvPrefix: this.kvPrefixes.portableInboxForwarding,
|
|
6965
|
+
outboxQueue: this.outboxQueue,
|
|
6966
|
+
startQueue: this.manuallyStartQueue ? void 0 : () => this._startQueueInternal(contextData),
|
|
6967
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
6968
|
+
getKeys: () => this.#getPortableGatewayKeyPairs(context, identifier, recipient),
|
|
6969
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
6970
|
+
signatures,
|
|
6971
|
+
options: this.portableInboxForwarding,
|
|
6972
|
+
meterProvider: this.meterProvider,
|
|
6973
|
+
tracerProvider: this.tracerProvider
|
|
6974
|
+
});
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
});
|
|
6978
|
+
}, span);
|
|
6979
|
+
}
|
|
6980
|
+
/**
|
|
6981
|
+
* Gets this server's gateway key pairs for the portable actor that owns
|
|
6982
|
+
* a portable inbox, which sign the activities forwarded from the inbox.
|
|
6983
|
+
* @param context The context.
|
|
6984
|
+
* @param identifier The identifier of the portable actor.
|
|
6985
|
+
* @param recipient The portable inbox.
|
|
6986
|
+
* @returns The key pairs, or an empty array if the actor has no gateway
|
|
6987
|
+
* keys on this server.
|
|
6988
|
+
*/
|
|
6989
|
+
async #getPortableGatewayKeyPairs(context, identifier, recipient) {
|
|
6990
|
+
const logger = getLogger([
|
|
6991
|
+
"fedify",
|
|
6992
|
+
"federation",
|
|
6993
|
+
"inbox"
|
|
6994
|
+
]);
|
|
6995
|
+
if (this.actorCallbacks?.keyPairsDispatcher == null || this.actorCallbacks.portableActorIdMapper == null) {
|
|
6996
|
+
logger.debug("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as gateway keys need both a key pairs dispatcher and a portable actor ID mapper.", { inbox: recipient.canonicalInboxId });
|
|
6997
|
+
return [];
|
|
6998
|
+
}
|
|
6999
|
+
const actorId = getCanonicalPortableId(recipient.actorId);
|
|
7000
|
+
if (actorId == null) return [];
|
|
7001
|
+
const keyPairs = await context.getActorKeyPairs(identifier);
|
|
7002
|
+
for (const { cryptographicKey } of keyPairs) {
|
|
7003
|
+
const owner = cryptographicKey.ownerId;
|
|
7004
|
+
const canonicalOwner = owner == null ? null : getCanonicalPortableId(owner);
|
|
7005
|
+
if (canonicalOwner === actorId) continue;
|
|
7006
|
+
if (canonicalOwner == null) logger.debug("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as the key pairs of the actor {identifier} are not the gateway keys of a portable actor.", {
|
|
7007
|
+
inbox: recipient.canonicalInboxId,
|
|
7008
|
+
identifier
|
|
7009
|
+
});
|
|
7010
|
+
else logger.warn("Forwarding activities from the portable inbox {inbox} without HTTP Signatures, as the key pairs of the actor {identifier} are the gateway keys of {owner}, not of the recipient {actorId}.", {
|
|
7011
|
+
inbox: recipient.canonicalInboxId,
|
|
7012
|
+
identifier,
|
|
7013
|
+
owner: canonicalOwner,
|
|
7014
|
+
actorId
|
|
7015
|
+
});
|
|
7016
|
+
return [];
|
|
7017
|
+
}
|
|
7018
|
+
return keyPairs.map(({ keyId, privateKey }) => ({
|
|
7019
|
+
keyId,
|
|
7020
|
+
privateKey
|
|
7021
|
+
}));
|
|
7022
|
+
}
|
|
7023
|
+
async #fetchHashlinkMedia(request, media, { onNotFound, contextData, span, metricState }) {
|
|
7024
|
+
metricState.endpoint = "hashlink_media";
|
|
7025
|
+
metricState.routeTemplate = HASHLINK_MEDIA_ROUTE_TEMPLATE;
|
|
7026
|
+
span.updateName(`${request.method} ${HASHLINK_MEDIA_ROUTE_TEMPLATE}`);
|
|
7027
|
+
if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method not allowed.", {
|
|
7028
|
+
status: 405,
|
|
7029
|
+
headers: {
|
|
7030
|
+
Allow: "GET, HEAD",
|
|
7031
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
7032
|
+
}
|
|
7033
|
+
});
|
|
7034
|
+
if (media.type === "malformed") {
|
|
7035
|
+
getLogger([
|
|
7036
|
+
"fedify",
|
|
7037
|
+
"federation",
|
|
7038
|
+
"hashlinkMedia"
|
|
7039
|
+
]).debug("Malformed FEP-ef61 hashlink media request {url}: {error}", {
|
|
7040
|
+
url: request.url,
|
|
7041
|
+
error: media.error
|
|
7042
|
+
});
|
|
7043
|
+
return new Response(request.method === "HEAD" ? null : "Malformed hashlink.", {
|
|
7044
|
+
status: 400,
|
|
7045
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
7046
|
+
});
|
|
7047
|
+
}
|
|
7048
|
+
const context = this.#createContext(request, contextData);
|
|
7049
|
+
const response = await this.hashlinkMediaDispatcher(context, media.media);
|
|
7050
|
+
if (response == null) return await onNotFound(request);
|
|
7051
|
+
return await discardBodyForHead(request, response, false);
|
|
7052
|
+
}
|
|
7053
|
+
};
|
|
7054
|
+
const PORTABLE_GATEWAY_ROUTE_PREFIX = "/.well-known/apgateway/{did}";
|
|
7055
|
+
/**
|
|
7056
|
+
* Gets the origin that a followers collection request filters followers by,
|
|
7057
|
+
* i.e., its normalized `base-url` query parameter (FEP-8fcf).
|
|
7058
|
+
* @returns The origin with a trailing slash, or `null` if the parameter is
|
|
7059
|
+
* missing or invalid.
|
|
7060
|
+
*/
|
|
7061
|
+
function getFollowersBaseUrl(url) {
|
|
7062
|
+
const baseUrl = url.searchParams.get("base-url");
|
|
7063
|
+
if (baseUrl == null) return null;
|
|
7064
|
+
try {
|
|
7065
|
+
return `${new URL(baseUrl).origin}/`;
|
|
7066
|
+
} catch {
|
|
7067
|
+
return null;
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
function getFollowersFilterPredicate(baseUrl) {
|
|
7071
|
+
return (i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl);
|
|
7072
|
+
}
|
|
7073
|
+
const ACTOR_COLLECTION_ROUTE_ERRORS = {
|
|
7074
|
+
outbox: "No outbox dispatcher registered.",
|
|
7075
|
+
following: "No following collection path registered.",
|
|
7076
|
+
followers: "No followers collection path registered.",
|
|
7077
|
+
liked: "No liked collection path registered.",
|
|
7078
|
+
featured: "No featured collection path registered.",
|
|
7079
|
+
featuredTags: "No featured tags collection path registered."
|
|
4394
7080
|
};
|
|
7081
|
+
/**
|
|
7082
|
+
* Checks that the authority of a portable ID to build is given, either
|
|
7083
|
+
* explicitly or by the FEP-ef61 gateway request being handled.
|
|
7084
|
+
* @throws {TypeError} If the authority is missing.
|
|
7085
|
+
*/
|
|
7086
|
+
function requirePortableAuthority(authority) {
|
|
7087
|
+
if (authority == null) throw new TypeError("The authority of a portable ID is required outside an FEP-ef61 gateway request.");
|
|
7088
|
+
return authority;
|
|
7089
|
+
}
|
|
7090
|
+
/**
|
|
7091
|
+
* Removes the body of a response to a `HEAD` request.
|
|
7092
|
+
* @param request The request.
|
|
7093
|
+
* @param response The response to the request.
|
|
7094
|
+
* @param waitForCancel Whether to wait until the discarded body is cancelled.
|
|
7095
|
+
* @returns The response without a body if the request is a `HEAD` request,
|
|
7096
|
+
* or the response itself otherwise.
|
|
7097
|
+
*/
|
|
7098
|
+
async function discardBodyForHead(request, response, waitForCancel = true) {
|
|
7099
|
+
if (request.method !== "HEAD" || response.body == null) return response;
|
|
7100
|
+
const cancelled = response.body.cancel();
|
|
7101
|
+
if (waitForCancel) await cancelled;
|
|
7102
|
+
else cancelled.catch((error) => {
|
|
7103
|
+
getLogger([
|
|
7104
|
+
"fedify",
|
|
7105
|
+
"federation",
|
|
7106
|
+
"http"
|
|
7107
|
+
]).debug("Failed to cancel the body of a response to {method} {url}: {error}", {
|
|
7108
|
+
method: request.method,
|
|
7109
|
+
url: request.url,
|
|
7110
|
+
error
|
|
7111
|
+
});
|
|
7112
|
+
});
|
|
7113
|
+
return new Response(null, {
|
|
7114
|
+
status: response.status,
|
|
7115
|
+
statusText: response.statusText,
|
|
7116
|
+
headers: response.headers
|
|
7117
|
+
});
|
|
7118
|
+
}
|
|
4395
7119
|
function getEndpointCategory(routeName) {
|
|
4396
7120
|
if (routeName.startsWith("object:")) return "object";
|
|
4397
7121
|
if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
|
|
@@ -4439,12 +7163,28 @@ function getCollectionMetricRoute(routeName) {
|
|
|
4439
7163
|
}
|
|
4440
7164
|
}
|
|
4441
7165
|
const FANOUT_THRESHOLD = 5;
|
|
7166
|
+
/**
|
|
7167
|
+
* Creates the FEP-ef61 portable object verifier that a context applies to
|
|
7168
|
+
* portable objects it dereferences: `verifyPortableObject()` with the given
|
|
7169
|
+
* loaders and the federation's tracer provider as defaults. Other options,
|
|
7170
|
+
* such as the document URL, gateways, and referrer, are passed through as
|
|
7171
|
+
* they are.
|
|
7172
|
+
*/
|
|
7173
|
+
function createPortableObjectVerifier(documentLoader, contextLoader, federation) {
|
|
7174
|
+
return (document, options = {}) => verifyPortableObject(document, {
|
|
7175
|
+
...options,
|
|
7176
|
+
documentLoader: options.documentLoader ?? documentLoader,
|
|
7177
|
+
contextLoader: options.contextLoader ?? contextLoader,
|
|
7178
|
+
tracerProvider: options.tracerProvider ?? federation.tracerProvider
|
|
7179
|
+
});
|
|
7180
|
+
}
|
|
4442
7181
|
var ContextImpl = class ContextImpl {
|
|
4443
7182
|
url;
|
|
4444
7183
|
federation;
|
|
4445
7184
|
data;
|
|
4446
7185
|
documentLoader;
|
|
4447
7186
|
contextLoader;
|
|
7187
|
+
verifyPortableObject;
|
|
4448
7188
|
invokedFromActorKeyPairsDispatcher;
|
|
4449
7189
|
#codec;
|
|
4450
7190
|
constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
|
|
@@ -4453,6 +7193,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4453
7193
|
this.data = data;
|
|
4454
7194
|
this.documentLoader = documentLoader;
|
|
4455
7195
|
this.contextLoader = contextLoader;
|
|
7196
|
+
this.verifyPortableObject = createPortableObjectVerifier(documentLoader, contextLoader, federation);
|
|
4456
7197
|
this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
|
|
4457
7198
|
}
|
|
4458
7199
|
/**
|
|
@@ -4521,22 +7262,43 @@ var ContextImpl = class ContextImpl {
|
|
|
4521
7262
|
return new URL(path, this.canonicalOrigin);
|
|
4522
7263
|
}
|
|
4523
7264
|
getActorUri(identifier) {
|
|
7265
|
+
return new URL(this.#getActorPath(identifier), this.canonicalOrigin);
|
|
7266
|
+
}
|
|
7267
|
+
getPortableActorUri(identifier, authority) {
|
|
7268
|
+
const path = this.#getActorPath(identifier);
|
|
7269
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
7270
|
+
}
|
|
7271
|
+
#getActorPath(identifier) {
|
|
4524
7272
|
const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
|
|
4525
7273
|
if (path == null) throw new RouterError("No actor dispatcher registered.");
|
|
4526
|
-
return
|
|
7274
|
+
return path;
|
|
4527
7275
|
}
|
|
4528
7276
|
getObjectUri(cls, values) {
|
|
7277
|
+
return new URL(this.#getObjectPath(cls, values), this.canonicalOrigin);
|
|
7278
|
+
}
|
|
7279
|
+
getPortableObjectUri(cls, values, authority) {
|
|
7280
|
+
const path = this.#getObjectPath(cls, values);
|
|
7281
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
7282
|
+
}
|
|
7283
|
+
#getObjectPath(cls, values) {
|
|
4529
7284
|
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
|
4530
7285
|
if (callbacks == null) throw new RouterError("No object dispatcher registered.");
|
|
4531
7286
|
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
|
4532
7287
|
const path = this.federation.router.build(`object:${cls.typeId.href}`, values);
|
|
4533
7288
|
if (path == null) throw new RouterError("No object dispatcher registered.");
|
|
4534
|
-
return
|
|
7289
|
+
return path;
|
|
7290
|
+
}
|
|
7291
|
+
#getActorCollectionPath(route, identifier) {
|
|
7292
|
+
const path = this.federation.router.build(route, { identifier });
|
|
7293
|
+
if (path == null) throw new RouterError(ACTOR_COLLECTION_ROUTE_ERRORS[route]);
|
|
7294
|
+
return path;
|
|
4535
7295
|
}
|
|
4536
7296
|
getOutboxUri(identifier) {
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
7297
|
+
return new URL(this.#getActorCollectionPath("outbox", identifier), this.canonicalOrigin);
|
|
7298
|
+
}
|
|
7299
|
+
getPortableOutboxUri(identifier, authority) {
|
|
7300
|
+
const path = this.#getActorCollectionPath("outbox", identifier);
|
|
7301
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4540
7302
|
}
|
|
4541
7303
|
getMediaUploaderUri(identifier) {
|
|
4542
7304
|
const path = this.federation.router.build("mediaUploader", { identifier });
|
|
@@ -4553,42 +7315,75 @@ var ContextImpl = class ContextImpl {
|
|
|
4553
7315
|
if (path == null) throw new RouterError("No inbox path registered.");
|
|
4554
7316
|
return new URL(path, this.canonicalOrigin);
|
|
4555
7317
|
}
|
|
7318
|
+
getPortableInboxUri(identifier, authority) {
|
|
7319
|
+
const path = this.federation.router.build("inbox", { identifier });
|
|
7320
|
+
if (path == null) throw new RouterError("No inbox path registered.");
|
|
7321
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
7322
|
+
}
|
|
4556
7323
|
getFollowingUri(identifier) {
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
7324
|
+
return new URL(this.#getActorCollectionPath("following", identifier), this.canonicalOrigin);
|
|
7325
|
+
}
|
|
7326
|
+
getPortableFollowingUri(identifier, authority) {
|
|
7327
|
+
const path = this.#getActorCollectionPath("following", identifier);
|
|
7328
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4560
7329
|
}
|
|
4561
7330
|
getFollowersUri(identifier) {
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
7331
|
+
return new URL(this.#getActorCollectionPath("followers", identifier), this.canonicalOrigin);
|
|
7332
|
+
}
|
|
7333
|
+
getPortableFollowersUri(identifier, authority) {
|
|
7334
|
+
const path = this.#getActorCollectionPath("followers", identifier);
|
|
7335
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4565
7336
|
}
|
|
4566
7337
|
getLikedUri(identifier) {
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
7338
|
+
return new URL(this.#getActorCollectionPath("liked", identifier), this.canonicalOrigin);
|
|
7339
|
+
}
|
|
7340
|
+
getPortableLikedUri(identifier, authority) {
|
|
7341
|
+
const path = this.#getActorCollectionPath("liked", identifier);
|
|
7342
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4570
7343
|
}
|
|
4571
7344
|
getFeaturedUri(identifier) {
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
7345
|
+
return new URL(this.#getActorCollectionPath("featured", identifier), this.canonicalOrigin);
|
|
7346
|
+
}
|
|
7347
|
+
getPortableFeaturedUri(identifier, authority) {
|
|
7348
|
+
const path = this.#getActorCollectionPath("featured", identifier);
|
|
7349
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4575
7350
|
}
|
|
4576
7351
|
getFeaturedTagsUri(identifier) {
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
7352
|
+
return new URL(this.#getActorCollectionPath("featuredTags", identifier), this.canonicalOrigin);
|
|
7353
|
+
}
|
|
7354
|
+
getPortableFeaturedTagsUri(identifier, authority) {
|
|
7355
|
+
const path = this.#getActorCollectionPath("featuredTags", identifier);
|
|
7356
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
4580
7357
|
}
|
|
4581
7358
|
getCollectionUri(name, values) {
|
|
4582
7359
|
const path = this.federation.getCollectionPath(name, values);
|
|
4583
7360
|
if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
|
|
4584
7361
|
return new URL(path, this.canonicalOrigin);
|
|
4585
7362
|
}
|
|
4586
|
-
|
|
7363
|
+
getPortableCollectionUri(name, values, authority) {
|
|
7364
|
+
const path = this.federation.getCollectionPath(name, values);
|
|
7365
|
+
if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
|
|
7366
|
+
return buildPortableUri(requirePortableAuthority(authority), path);
|
|
7367
|
+
}
|
|
7368
|
+
parseUri(uri, options = {}) {
|
|
4587
7369
|
if (uri == null) return null;
|
|
4588
|
-
if (uri.origin
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
7370
|
+
if (uri.origin === this.origin || uri.origin === this.canonicalOrigin) {
|
|
7371
|
+
const route = this.federation.router.route(uri.pathname);
|
|
7372
|
+
if (route != null) return this.#parseRoute(route);
|
|
7373
|
+
}
|
|
7374
|
+
if (!options.portable) return null;
|
|
7375
|
+
const portable = parsePortableId(uri);
|
|
7376
|
+
if (portable == null) return null;
|
|
7377
|
+
const route = this.federation.router.route(portable.path);
|
|
7378
|
+
if (route == null || route.name === "sharedInbox") return null;
|
|
7379
|
+
const result = this.#parseRoute(route);
|
|
7380
|
+
return result == null ? null : {
|
|
7381
|
+
...result,
|
|
7382
|
+
authority: portable.authority
|
|
7383
|
+
};
|
|
7384
|
+
}
|
|
7385
|
+
#parseRoute(route) {
|
|
7386
|
+
if (route.name === "sharedInbox") return {
|
|
4592
7387
|
type: "inbox",
|
|
4593
7388
|
identifier: void 0
|
|
4594
7389
|
};
|
|
@@ -4636,8 +7431,10 @@ var ContextImpl = class ContextImpl {
|
|
|
4636
7431
|
const collectionRegex = new RegExp(`^(${["collection", "orderedCollection"].join("|")}):(.*)$`);
|
|
4637
7432
|
const match = route.name.match(collectionRegex);
|
|
4638
7433
|
if (match !== null) {
|
|
4639
|
-
const [, type
|
|
7434
|
+
const [, type] = match;
|
|
7435
|
+
const name = this.federation.getCollectionName(route.name);
|
|
4640
7436
|
const cls = this.federation.collectionTypeIds[name];
|
|
7437
|
+
if (cls == null) return null;
|
|
4641
7438
|
return {
|
|
4642
7439
|
type,
|
|
4643
7440
|
name,
|
|
@@ -4658,14 +7455,15 @@ var ContextImpl = class ContextImpl {
|
|
|
4658
7455
|
getActorKeyPairsIdentifier: identifier,
|
|
4659
7456
|
actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
|
|
4660
7457
|
});
|
|
7458
|
+
const portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
4661
7459
|
let keyPairs;
|
|
4662
7460
|
try {
|
|
4663
|
-
keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
|
7461
|
+
keyPairs = await this.getKeyPairsFromIdentifier(identifier, portableActor);
|
|
4664
7462
|
} catch (_) {
|
|
4665
7463
|
logger.warn("No actor key pairs dispatcher registered.");
|
|
4666
7464
|
return [];
|
|
4667
7465
|
}
|
|
4668
|
-
const owner = this.getActorUri(identifier);
|
|
7466
|
+
const owner = portableActor?.id ?? this.getActorUri(identifier);
|
|
4669
7467
|
const result = [];
|
|
4670
7468
|
let i = 1;
|
|
4671
7469
|
for (const keyPair of keyPairs) {
|
|
@@ -4677,7 +7475,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4677
7475
|
publicKey: keyPair.publicKey
|
|
4678
7476
|
}),
|
|
4679
7477
|
multikey: new Multikey({
|
|
4680
|
-
id: new URL(`#multikey-${i}`, owner),
|
|
7478
|
+
id: portableActor == null ? new URL(`#multikey-${i}`, owner) : keyPair.keyId,
|
|
4681
7479
|
controller: owner,
|
|
4682
7480
|
publicKey: keyPair.publicKey
|
|
4683
7481
|
})
|
|
@@ -4687,7 +7485,26 @@ var ContextImpl = class ContextImpl {
|
|
|
4687
7485
|
}
|
|
4688
7486
|
return result;
|
|
4689
7487
|
}
|
|
4690
|
-
|
|
7488
|
+
/**
|
|
7489
|
+
* Resolves the portable actor that the actor with the given identifier is,
|
|
7490
|
+
* through the portable actor ID mapper, along with the base of its gateway
|
|
7491
|
+
* key IDs on this server.
|
|
7492
|
+
* @param identifier The actor's identifier.
|
|
7493
|
+
* @returns The portable actor's ID and key ID base, or `null` if the actor
|
|
7494
|
+
* is not portable.
|
|
7495
|
+
* @throws {TypeError} If the mapper returns an invalid portable actor ID.
|
|
7496
|
+
*/
|
|
7497
|
+
async getPortableActorKeyOwner(identifier) {
|
|
7498
|
+
const mapper = this.federation.actorCallbacks?.portableActorIdMapper;
|
|
7499
|
+
if (mapper == null) return null;
|
|
7500
|
+
const id = await mapper(this, identifier);
|
|
7501
|
+
if (id == null) return null;
|
|
7502
|
+
return {
|
|
7503
|
+
id,
|
|
7504
|
+
keyBase: getGatewayKeyBase(id, this.canonicalOrigin)
|
|
7505
|
+
};
|
|
7506
|
+
}
|
|
7507
|
+
async getKeyPairsFromIdentifier(identifier, portableActor) {
|
|
4691
7508
|
const logger = getLogger([
|
|
4692
7509
|
"fedify",
|
|
4693
7510
|
"federation",
|
|
@@ -4704,6 +7521,8 @@ var ContextImpl = class ContextImpl {
|
|
|
4704
7521
|
}
|
|
4705
7522
|
throw error;
|
|
4706
7523
|
}
|
|
7524
|
+
if (portableActor === void 0) portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
7525
|
+
const keyBase = portableActor?.keyBase ?? actorUri;
|
|
4707
7526
|
const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
|
|
4708
7527
|
...this,
|
|
4709
7528
|
invokedFromActorKeyPairsDispatcher: { identifier }
|
|
@@ -4714,7 +7533,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4714
7533
|
for (const keyPair of keyPairs) {
|
|
4715
7534
|
result.push({
|
|
4716
7535
|
...keyPair,
|
|
4717
|
-
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`,
|
|
7536
|
+
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, keyBase)
|
|
4718
7537
|
});
|
|
4719
7538
|
i++;
|
|
4720
7539
|
}
|
|
@@ -4760,6 +7579,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4760
7579
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
4761
7580
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4762
7581
|
meterProvider: options.meterProvider ?? this.meterProvider,
|
|
7582
|
+
verifyPortableObject: options.verifyPortableObject ?? this.verifyPortableObject,
|
|
4763
7583
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4764
7584
|
});
|
|
4765
7585
|
}
|
|
@@ -4767,7 +7587,8 @@ var ContextImpl = class ContextImpl {
|
|
|
4767
7587
|
return traverseCollection(collection, {
|
|
4768
7588
|
...options,
|
|
4769
7589
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
4770
|
-
contextLoader: options.contextLoader ?? this.contextLoader
|
|
7590
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
7591
|
+
verifyPortableObject: options.verifyPortableObject ?? this.verifyPortableObject
|
|
4771
7592
|
});
|
|
4772
7593
|
}
|
|
4773
7594
|
lookupNodeInfo(url, options = {}) {
|
|
@@ -4885,18 +7706,18 @@ var ContextImpl = class ContextImpl {
|
|
|
4885
7706
|
});
|
|
4886
7707
|
throw new TypeError("The activity to send must have at least one actor property.");
|
|
4887
7708
|
}
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
7709
|
+
const { activity: signedActivity, proofCreated } = await signOutgoingActivity(activity, actorKeyPairs == null ? keys.map(({ keyId, privateKey }) => ({
|
|
7710
|
+
verificationMethod: keyId,
|
|
7711
|
+
privateKey
|
|
7712
|
+
})) : actorKeyPairs.flatMap((kp) => kp.multikey.id == null ? [] : [{
|
|
7713
|
+
verificationMethod: kp.multikey.id,
|
|
7714
|
+
privateKey: kp.privateKey
|
|
7715
|
+
}]), {
|
|
7716
|
+
contextLoader: this.contextLoader,
|
|
7717
|
+
tracerProvider: this.tracerProvider,
|
|
7718
|
+
appendToExistingProofs: actorKeyPairs != null
|
|
7719
|
+
});
|
|
7720
|
+
activity = signedActivity;
|
|
4900
7721
|
const inboxes = extractInboxes({
|
|
4901
7722
|
recipients: expandedRecipients,
|
|
4902
7723
|
preferSharedInbox: options.preferSharedInbox,
|
|
@@ -4914,13 +7735,27 @@ var ContextImpl = class ContextImpl {
|
|
|
4914
7735
|
activityId: activity.id?.href,
|
|
4915
7736
|
activity
|
|
4916
7737
|
});
|
|
7738
|
+
const activityJsonLd = await activity.toJsonLd({
|
|
7739
|
+
format: "compact",
|
|
7740
|
+
contextLoader: this.contextLoader
|
|
7741
|
+
});
|
|
7742
|
+
try {
|
|
7743
|
+
await warnCompatibleIdsInJson(activityJsonLd, this instanceof RequestContextImpl && !this.isInActorDispatcher() ? this : void 0);
|
|
7744
|
+
} catch (error) {
|
|
7745
|
+
logger.debug("Could not check the first gateway of activity {activityId}: {error}", {
|
|
7746
|
+
activityId: activity.id?.href,
|
|
7747
|
+
error
|
|
7748
|
+
});
|
|
7749
|
+
}
|
|
4917
7750
|
if (this.federation.fanoutQueue == null || options.immediate || options.fanout === "skip" || (options.fanout ?? "auto") === "auto" && globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
|
|
4918
7751
|
await this.federation.sendActivity(keys, inboxes, activity, {
|
|
4919
7752
|
...opts,
|
|
7753
|
+
activityJsonLd,
|
|
4920
7754
|
normalizeExistingProofs: proofCreated || options.normalizeExistingProofs
|
|
4921
7755
|
});
|
|
4922
7756
|
return true;
|
|
4923
7757
|
}
|
|
7758
|
+
assertSupportedCompoundProofShape(activityJsonLd, activity.id?.href);
|
|
4924
7759
|
const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
|
|
4925
7760
|
keyId: keyId.href,
|
|
4926
7761
|
privateKey: await exportJwk(privateKey)
|
|
@@ -4932,14 +7767,12 @@ var ContextImpl = class ContextImpl {
|
|
|
4932
7767
|
id: crypto.randomUUID(),
|
|
4933
7768
|
baseUrl: this.origin,
|
|
4934
7769
|
keys: keyJwkPairs,
|
|
4935
|
-
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox }]) => [k, {
|
|
7770
|
+
inboxes: globalThis.Object.fromEntries(globalThis.Object.entries(inboxes).map(([k, { actorIds, sharedInbox, portableInbox, gatewayInboxes }]) => [k, {
|
|
4936
7771
|
actorIds: [...actorIds],
|
|
4937
|
-
sharedInbox
|
|
7772
|
+
sharedInbox,
|
|
7773
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes)
|
|
4938
7774
|
}])),
|
|
4939
|
-
activity:
|
|
4940
|
-
format: "compact",
|
|
4941
|
-
contextLoader: this.contextLoader
|
|
4942
|
-
}),
|
|
7775
|
+
activity: activityJsonLd,
|
|
4943
7776
|
activityId: activity.id?.href,
|
|
4944
7777
|
activityType: getTypeId(activity).href,
|
|
4945
7778
|
collectionSync: opts.collectionSync,
|
|
@@ -5013,8 +7846,13 @@ var ContextImpl = class ContextImpl {
|
|
|
5013
7846
|
"inbox"
|
|
5014
7847
|
]);
|
|
5015
7848
|
const contextLoader = options.contextLoader ?? this.contextLoader;
|
|
5016
|
-
|
|
5017
|
-
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey,
|
|
7849
|
+
let json = await activity.toJsonLd({ contextLoader });
|
|
7850
|
+
const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, {
|
|
7851
|
+
documentLoader: this.documentLoader,
|
|
7852
|
+
contextLoader: this.contextLoader,
|
|
7853
|
+
tracerProvider: this.tracerProvider,
|
|
7854
|
+
keyTtl: this.federation.publicKeyTtl
|
|
7855
|
+
});
|
|
5018
7856
|
if (await verifyObject(Activity, json, {
|
|
5019
7857
|
contextLoader,
|
|
5020
7858
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
@@ -5047,7 +7885,7 @@ var ContextImpl = class ContextImpl {
|
|
|
5047
7885
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
5048
7886
|
});
|
|
5049
7887
|
return false;
|
|
5050
|
-
} else if (fetched.id
|
|
7888
|
+
} else if (fetched.id == null || !isSameObjectId(fetched.id, activity.id)) {
|
|
5051
7889
|
logger.debug("Fetched activity object has a different ID; failed to verify.", {
|
|
5052
7890
|
recipient,
|
|
5053
7891
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
@@ -5060,8 +7898,8 @@ var ContextImpl = class ContextImpl {
|
|
|
5060
7898
|
});
|
|
5061
7899
|
return false;
|
|
5062
7900
|
}
|
|
5063
|
-
const
|
|
5064
|
-
if (!fetched.actorIds.every((actor) => actor
|
|
7901
|
+
const activityOrigin = getAuthenticationOrigin(fetched.id);
|
|
7902
|
+
if (activityOrigin == null || !fetched.actorIds.every((actor) => getAuthenticationOrigin(actor) === activityOrigin)) {
|
|
5065
7903
|
logger.debug("Fetched activity object has actors from different origins; unable to verify.", {
|
|
5066
7904
|
recipient,
|
|
5067
7905
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
@@ -5070,6 +7908,7 @@ var ContextImpl = class ContextImpl {
|
|
|
5070
7908
|
}
|
|
5071
7909
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
5072
7910
|
activity = fetched;
|
|
7911
|
+
json = await fetched.toJsonLd({ contextLoader });
|
|
5073
7912
|
} else logger.debug("Object Integrity Proofs are verified.", {
|
|
5074
7913
|
recipient,
|
|
5075
7914
|
activity: json
|
|
@@ -5098,13 +7937,19 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5098
7937
|
#invokedFromActorDispatcher;
|
|
5099
7938
|
#invokedFromObjectDispatcher;
|
|
5100
7939
|
request;
|
|
7940
|
+
/** Whether a diagnostic actor lookup would re-enter the actor dispatcher. */
|
|
7941
|
+
isInActorDispatcher() {
|
|
7942
|
+
return this.#invokedFromActorDispatcher != null;
|
|
7943
|
+
}
|
|
5101
7944
|
url = void 0;
|
|
7945
|
+
portableRequest;
|
|
5102
7946
|
constructor(options) {
|
|
5103
7947
|
super(options);
|
|
5104
7948
|
this.#invokedFromActorDispatcher = options.invokedFromActorDispatcher;
|
|
5105
7949
|
this.#invokedFromObjectDispatcher = options.invokedFromObjectDispatcher;
|
|
5106
7950
|
this.request = options.request;
|
|
5107
7951
|
this.url = options.url;
|
|
7952
|
+
this.portableRequest = options.portableRequest;
|
|
5108
7953
|
}
|
|
5109
7954
|
clone(data) {
|
|
5110
7955
|
return new RequestContextImpl({
|
|
@@ -5116,9 +7961,40 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5116
7961
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
|
|
5117
7962
|
invokedFromActorDispatcher: this.#invokedFromActorDispatcher,
|
|
5118
7963
|
invokedFromObjectDispatcher: this.#invokedFromObjectDispatcher,
|
|
7964
|
+
portableRequest: this.portableRequest,
|
|
5119
7965
|
request: this.request
|
|
5120
7966
|
});
|
|
5121
7967
|
}
|
|
7968
|
+
getPortableActorUri(identifier, authority) {
|
|
7969
|
+
return super.getPortableActorUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7970
|
+
}
|
|
7971
|
+
getPortableObjectUri(cls, values, authority) {
|
|
7972
|
+
return super.getPortableObjectUri(cls, values, authority ?? this.portableRequest?.authority);
|
|
7973
|
+
}
|
|
7974
|
+
getPortableInboxUri(identifier, authority) {
|
|
7975
|
+
return super.getPortableInboxUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7976
|
+
}
|
|
7977
|
+
getPortableOutboxUri(identifier, authority) {
|
|
7978
|
+
return super.getPortableOutboxUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7979
|
+
}
|
|
7980
|
+
getPortableFollowingUri(identifier, authority) {
|
|
7981
|
+
return super.getPortableFollowingUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7982
|
+
}
|
|
7983
|
+
getPortableFollowersUri(identifier, authority) {
|
|
7984
|
+
return super.getPortableFollowersUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7985
|
+
}
|
|
7986
|
+
getPortableLikedUri(identifier, authority) {
|
|
7987
|
+
return super.getPortableLikedUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7988
|
+
}
|
|
7989
|
+
getPortableFeaturedUri(identifier, authority) {
|
|
7990
|
+
return super.getPortableFeaturedUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7991
|
+
}
|
|
7992
|
+
getPortableFeaturedTagsUri(identifier, authority) {
|
|
7993
|
+
return super.getPortableFeaturedTagsUri(identifier, authority ?? this.portableRequest?.authority);
|
|
7994
|
+
}
|
|
7995
|
+
getPortableCollectionUri(name, values, authority) {
|
|
7996
|
+
return super.getPortableCollectionUri(name, values, authority ?? this.portableRequest?.authority);
|
|
7997
|
+
}
|
|
5122
7998
|
async getActor(identifier, options) {
|
|
5123
7999
|
if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
|
|
5124
8000
|
if (this.#invokedFromActorDispatcher != null) getLogger([
|
|
@@ -5136,7 +8012,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5136
8012
|
if (actor instanceof Tombstone && options?.tombstone !== "passthrough") return null;
|
|
5137
8013
|
return actor;
|
|
5138
8014
|
}
|
|
5139
|
-
async getObject(cls, values) {
|
|
8015
|
+
async getObject(cls, values, options) {
|
|
5140
8016
|
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
|
5141
8017
|
if (callbacks == null) throw new Error("No object dispatcher registered.");
|
|
5142
8018
|
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
|
@@ -5146,13 +8022,15 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5146
8022
|
actorDispatcherClass: this.#invokedFromObjectDispatcher.cls.name,
|
|
5147
8023
|
actorDispatcherValues: this.#invokedFromObjectDispatcher.values
|
|
5148
8024
|
});
|
|
5149
|
-
|
|
8025
|
+
const object = await callbacks.dispatcher(new RequestContextImpl({
|
|
5150
8026
|
...this,
|
|
5151
8027
|
invokedFromObjectDispatcher: {
|
|
5152
8028
|
cls,
|
|
5153
8029
|
values
|
|
5154
8030
|
}
|
|
5155
8031
|
}), values);
|
|
8032
|
+
if (object instanceof Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
|
|
8033
|
+
return object;
|
|
5156
8034
|
}
|
|
5157
8035
|
#signedKey = void 0;
|
|
5158
8036
|
async getSignedKey(options = {}) {
|
|
@@ -5162,6 +8040,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5162
8040
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
5163
8041
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
5164
8042
|
timeWindow: this.federation.signatureTimeWindow,
|
|
8043
|
+
maxSignatures: this.federation.maxHttpSignatures,
|
|
5165
8044
|
meterProvider: this.meterProvider,
|
|
5166
8045
|
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
5167
8046
|
});
|
|
@@ -5193,6 +8072,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5193
8072
|
throw error;
|
|
5194
8073
|
}
|
|
5195
8074
|
}
|
|
8075
|
+
async isSignedByAudience(object, options = {}) {
|
|
8076
|
+
return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
|
|
8077
|
+
}
|
|
5196
8078
|
};
|
|
5197
8079
|
function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
|
|
5198
8080
|
return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
|
|
@@ -5280,16 +8162,16 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5280
8162
|
if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
|
|
5281
8163
|
else logger.debug("Forwarding activity immediately without queue since queue is not set.");
|
|
5282
8164
|
const promises = [];
|
|
5283
|
-
for (const inbox in inboxes) promises.push(
|
|
8165
|
+
for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
|
|
8166
|
+
allowPrivateAddress: ctx.federation.allowPrivateAddress,
|
|
5284
8167
|
keys,
|
|
5285
8168
|
activity: ctx.activity,
|
|
5286
8169
|
activityId: ctx.activityId,
|
|
5287
8170
|
activityType: ctx.activityType,
|
|
5288
|
-
inbox: new URL(inbox),
|
|
5289
8171
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
5290
8172
|
meterProvider: ctx.meterProvider,
|
|
5291
8173
|
tracerProvider: ctx.tracerProvider,
|
|
5292
|
-
specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock)
|
|
8174
|
+
specDeterminer: new KvSpecDeterminer(ctx.federation.kv, ctx.federation.kvPrefixes.httpMessageSignaturesSpec, ctx.federation.firstKnock, { specTtl: ctx.federation.httpMessageSignaturesSpecTtl })
|
|
5293
8175
|
}));
|
|
5294
8176
|
await Promise.all(promises);
|
|
5295
8177
|
return true;
|
|
@@ -5313,7 +8195,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5313
8195
|
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
5314
8196
|
const messages = [];
|
|
5315
8197
|
for (const inbox in inboxes) {
|
|
5316
|
-
const
|
|
8198
|
+
const { portableInbox, gatewayInboxes } = inboxes[inbox];
|
|
5317
8199
|
const message = {
|
|
5318
8200
|
type: "outbox",
|
|
5319
8201
|
id: crypto.randomUUID(),
|
|
@@ -5328,7 +8210,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5328
8210
|
started,
|
|
5329
8211
|
attempt: 0,
|
|
5330
8212
|
headers: {},
|
|
5331
|
-
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${
|
|
8213
|
+
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
|
|
8214
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
5332
8215
|
traceContext: carrier
|
|
5333
8216
|
};
|
|
5334
8217
|
messages.push({
|
|
@@ -5460,16 +8343,18 @@ var KvSpecDeterminer = class {
|
|
|
5460
8343
|
kv;
|
|
5461
8344
|
prefix;
|
|
5462
8345
|
defaultSpec;
|
|
5463
|
-
|
|
8346
|
+
specTtl;
|
|
8347
|
+
constructor(kv, prefix, defaultSpec = "rfc9421", options = {}) {
|
|
5464
8348
|
this.kv = kv;
|
|
5465
8349
|
this.prefix = prefix;
|
|
5466
8350
|
this.defaultSpec = defaultSpec;
|
|
8351
|
+
this.specTtl = options.specTtl ?? Temporal.Duration.from({ days: 90 });
|
|
5467
8352
|
}
|
|
5468
8353
|
async determineSpec(origin) {
|
|
5469
8354
|
return await this.kv.get([...this.prefix, origin]) ?? this.defaultSpec;
|
|
5470
8355
|
}
|
|
5471
8356
|
async rememberSpec(origin, spec) {
|
|
5472
|
-
await this.kv.set([...this.prefix, origin], spec);
|
|
8357
|
+
await this.kv.set([...this.prefix, origin], spec, { ttl: this.specTtl });
|
|
5473
8358
|
}
|
|
5474
8359
|
};
|
|
5475
8360
|
function notFound(_request) {
|
|
@@ -5506,4 +8391,4 @@ function getRequestId(request) {
|
|
|
5506
8391
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
5507
8392
|
}
|
|
5508
8393
|
//#endregion
|
|
5509
|
-
export { handleNodeInfo as _, OutboxContextImpl as a,
|
|
8394
|
+
export { getEmbeddedFirstGateway as C, handleNodeInfoJrd as D, handleNodeInfo as E, actorDehydrator as O, getCompactRootId as S, handleBenchmarkTrigger as T, coversDelivery as _, OutboxContextImpl as a, PORTABLE_OBJECT_CONTENT_TYPE as b, assertPortableActorActivity as c, handleCustomCollection as d, handleInbox as f, respondWithObjectIfAcceptable as g, respondWithObject as h, KvSpecDeterminer as i, autoIdAssigner as k, handleActor as l, handleOutbox as m, FederationImpl as n, createFederation as o, handleObject as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCollection as u, forwardPortableInboxActivity as v, warnCompatibleIdsInJson as w, parseHashlinkGatewayRequest as x, resolvePortableInboxForwardingOptions as y };
|