@fedify/fedify 2.4.0-pr.934.40 → 2.4.0
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 +48 -40
- 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-BBPwtB2v.mjs → builder-lJDh222H.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-zAjqsHiN.mjs +989 -0
- package/dist/{context-Cjnr0ig1.d.ts → context-7NyiqIcg.d.ts} +1600 -25
- package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
- package/dist/{context-C05Rgxmu.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
- package/dist/{deno-D3c0o_N0.mjs → deno-D8wr4ikF.mjs} +1 -1
- package/dist/dist-DJQM4u7i.mjs +189 -0
- package/dist/docloader-TCvsU9vF.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 +387 -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 -540
- package/dist/federation/mod.d.cts +5 -5
- package/dist/federation/mod.d.ts +5 -5
- package/dist/federation/mod.js +1 -1
- package/dist/federation/mq.test.mjs +104 -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 +5 -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-BW7WQltk.cjs → federation-Dfl-i7Jt.cjs} +6098 -895
- package/dist/{http-Dm0JAUa_.mjs → http-BAgKAAGY.mjs} +397 -168
- package/dist/http-CYO4wjB5.cjs +10630 -0
- package/dist/http-RW_i_Nkq.js +6527 -0
- package/dist/key-C1nv7_jD.d.ts +1125 -0
- package/dist/key-D979vduq.mjs +951 -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-BxUZRBC2.js → kv-cache-Cqfdytdd.js} +54 -16
- package/dist/{kv-cache-B7H7lZ84.mjs → kv-cache-Gh05Xv-P.mjs} +1 -1
- package/dist/{kv-cache-D-hbjJAD.cjs → kv-cache-zd0dNGxi.cjs} +55 -19
- package/dist/{ld-DAPAVL_7.mjs → ld-wmqC-95l.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-BMaatuZi.mjs → metrics-DPdi_XK7.mjs} +3 -2
- package/dist/metrics-DVKYY6K1.d.cts +36 -0
- package/dist/metrics-DoRtI0qU.d.ts +36 -0
- package/dist/{middleware-O2LHXFEk.mjs → middleware-CqQ5Qahj.mjs} +3155 -269
- package/dist/{middleware-CfZR_riY.js → middleware-DbCI_eni.js} +5527 -865
- package/dist/{middleware-DfPBAh9L.mjs → middleware-DjEMIAP5.mjs} +1 -1
- package/dist/{mod-DdONo-ep.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-CNef4in0.d.ts → mod-DEZXwV6J.d.ts} +9 -3
- package/dist/{mod-BrpVsSfY.d.cts → mod-Dw-nPxQh.d.cts} +9 -3
- package/dist/{mod-BsCE5Ihq.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 +8 -8
- 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-BfygwLNy.mjs +267 -0
- package/dist/portable-collection-DSrR0BSV.js +670 -0
- package/dist/portable-collection-L8jCHlci.cjs +686 -0
- package/dist/portable-collection-idA006Sj.mjs +418 -0
- package/dist/portable-key-CA6u-dST.mjs +549 -0
- package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
- package/dist/proof-DeC9pb6t.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-1tcGhW1h.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-yE5EcfNX.mjs → temporal-DA7r1fm3.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 +1604 -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 +9 -9
- package/dist/assert_rejects-EgstcQy5.mjs +0 -82
- package/dist/docloader-BCecHIxU.mjs +0 -49
- package/dist/http-Ctb_KhZ8.js +0 -2483
- package/dist/http-VyDTd4G3.d.cts +0 -553
- package/dist/http-jr0TkgZe.cjs +0 -2694
- package/dist/http-lf8Hsd91.d.ts +0 -553
- package/dist/key-C_gNKtxm.mjs +0 -453
- package/dist/keycache-_DHqz5z_.mjs +0 -97
- package/dist/metrics-DN13mCAw.d.cts +0 -294
- package/dist/metrics-v_e0n5GQ.d.ts +0 -294
- package/dist/mod-B0hW12_O.d.cts +0 -307
- package/dist/mod-COIAjwRS.d.ts +0 -307
- package/dist/owner-Dcx-czqn.mjs +0 -120
- package/dist/proof-BkAxvrjt.mjs +0 -316
- package/dist/proof-D-H_PnJW.js +0 -1415
- package/dist/proof-NY0D8Oux.cjs +0 -1562
- package/dist/send-D_j7E-oC.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/{mq-Bo7Etgh6.mjs → mq-Dm1Wmtm-.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,38 +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
|
|
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-D8wr4ikF.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-DPdi_XK7.mjs";
|
|
9
|
+
import { t as formatAcceptSignature } from "./accept-CPVqki78.mjs";
|
|
10
|
+
import { c as validateCryptoKey, n as exportJwk, s as importJwk } from "./key-D979vduq.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-wmqC-95l.mjs";
|
|
13
|
+
import { a as signObject, c as verifyPortableObjectProof, i as hasProofLike, s as verifyObject, t as InvalidPortableObjectIdError, u as verifyPortableObjectProofWithRoot } from "./proof-DeC9pb6t.mjs";
|
|
14
|
+
import { a as listRequestSignatures, c as selectRequestSignature, d as validateMaxSignatures, f as verifyRequest, p as verifyRequestDetailed, s as parseRfc9421SignatureInput } from "./http-BAgKAAGY.mjs";
|
|
15
|
+
import { t as getAuthenticatedDocumentLoader } from "./docloader-TCvsU9vF.mjs";
|
|
16
|
+
import { n as kvCache } from "./kv-cache-Gh05Xv-P.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-BfygwLNy.mjs";
|
|
20
|
+
import { t as verifyPortableObject } from "./portable-collection-idA006Sj.mjs";
|
|
21
|
+
import { a as getOrderingDestination, i as sendActivityThroughGateways, n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-1tcGhW1h.mjs";
|
|
22
|
+
import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-lJDh222H.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-zAjqsHiN.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-DA7r1fm3.mjs";
|
|
29
|
+
import { t as createExponentialBackoffPolicy } from "./retry-zX5xw5e0.mjs";
|
|
30
|
+
import { t as TaskCodec } from "./codec-C6rTrpIp.mjs";
|
|
26
31
|
import { getLogger, withContext } from "@logtape/logtape";
|
|
27
32
|
import { RouterError } from "@fedify/uri-template";
|
|
28
|
-
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";
|
|
29
34
|
import { lookupWebFinger } from "@fedify/webfinger";
|
|
30
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";
|
|
31
38
|
import { uniq } from "es-toolkit";
|
|
32
|
-
import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
|
|
33
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";
|
|
34
41
|
import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
|
|
35
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
|
|
36
148
|
//#region src/compat/transformers.ts
|
|
37
149
|
const logger$2 = getLogger([
|
|
38
150
|
"fedify",
|
|
@@ -353,6 +465,180 @@ function serializeDataPointType(dataPointType) {
|
|
|
353
465
|
}
|
|
354
466
|
}
|
|
355
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
|
|
356
642
|
//#region src/federation/inbox.ts
|
|
357
643
|
async function routeActivity({ context: ctx, json, originalJson, normalizedActivity, ldSignatureVerified, activity, recipient, inboxListeners, inboxContextFactory, listenerInboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, meterProvider, tracerProvider, idempotencyStrategy }) {
|
|
358
644
|
const logger = getLogger([
|
|
@@ -511,8 +797,602 @@ async function routeActivity({ context: ctx, json, originalJson, normalizedActiv
|
|
|
511
797
|
});
|
|
512
798
|
}
|
|
513
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
|
|
514
1388
|
//#region src/federation/handler.ts
|
|
515
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
|
+
};
|
|
516
1396
|
function isRemoteContextLoadingFailure$1(error) {
|
|
517
1397
|
return error instanceof Error && typeof error.details === "object" && error.details != null && error.details.code === "loading remote context failed";
|
|
518
1398
|
}
|
|
@@ -528,7 +1408,7 @@ function isInvalidJsonLdError(error) {
|
|
|
528
1408
|
return name === "UnsafeJsonLdError" || error instanceof InvalidContextReferenceError || isPermanentRemoteContextError$1(error) || name === "jsonld.SyntaxError" && !isRemoteContextLoadingFailure$1(error);
|
|
529
1409
|
}
|
|
530
1410
|
function isValidationTypeError(error) {
|
|
531
|
-
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));
|
|
532
1412
|
}
|
|
533
1413
|
function isPermanentActivityParseError(error) {
|
|
534
1414
|
return isInvalidJsonLdError(error) || isValidationTypeError(error);
|
|
@@ -595,12 +1475,221 @@ async function handleObject(request, { values, context, objectDispatcher, author
|
|
|
595
1475
|
if (!await authorizePredicate(context, values)) return await onUnauthorized(request);
|
|
596
1476
|
}
|
|
597
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));
|
|
598
1486
|
return new Response(JSON.stringify(jsonLd), { headers: {
|
|
599
1487
|
"Content-Type": "application/activity+json",
|
|
600
1488
|
Vary: "Accept"
|
|
601
1489
|
} });
|
|
602
1490
|
}
|
|
603
|
-
|
|
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([
|
|
604
1693
|
"inbox",
|
|
605
1694
|
"outbox",
|
|
606
1695
|
"following",
|
|
@@ -637,13 +1726,13 @@ function recordCollectionMetrics(meterProvider, base, result, options = {}) {
|
|
|
637
1726
|
* @param parameters The parameters for handling the collection.
|
|
638
1727
|
* @returns A promise that resolves to an HTTP response.
|
|
639
1728
|
*/
|
|
640
|
-
async function handleCollection(request, { name: name$
|
|
641
|
-
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, "_");
|
|
642
1731
|
tracerProvider = tracerProvider ?? trace.getTracerProvider();
|
|
643
1732
|
const tracer = tracerProvider.getTracer(name, version);
|
|
644
1733
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
645
1734
|
const metricBase = {
|
|
646
|
-
kind: getCollectionMetricKind(name$
|
|
1735
|
+
kind: getCollectionMetricKind(name$2),
|
|
647
1736
|
page: cursor != null,
|
|
648
1737
|
dispatcher: "built_in"
|
|
649
1738
|
};
|
|
@@ -661,8 +1750,13 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
661
1750
|
};
|
|
662
1751
|
try {
|
|
663
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
|
+
}
|
|
664
1756
|
let collection;
|
|
665
|
-
const baseUri = uriGetter(identifier);
|
|
1757
|
+
const baseUri = portable?.id ?? uriGetter(identifier);
|
|
1758
|
+
const pageBase = portable?.view ?? context.url;
|
|
1759
|
+
const attribution = portable?.attribution ?? null;
|
|
666
1760
|
if (cursor == null) {
|
|
667
1761
|
const firstCursor = await collectionCallbacks.firstCursor?.(context, identifier);
|
|
668
1762
|
const totalItems = filter == null ? await collectionCallbacks.counter?.(context, identifier) : void 0;
|
|
@@ -684,7 +1778,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
684
1778
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
685
1779
|
return await onNotFound(request);
|
|
686
1780
|
}
|
|
687
|
-
const items = filterCollectionItems(page.items, name$
|
|
1781
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
688
1782
|
itemCount = items.length;
|
|
689
1783
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
690
1784
|
return items;
|
|
@@ -702,29 +1796,31 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
702
1796
|
if (itemsOrResponse instanceof Response) return finish(itemsOrResponse, "not_found");
|
|
703
1797
|
collection = new OrderedCollection({
|
|
704
1798
|
id: baseUri,
|
|
705
|
-
|
|
1799
|
+
attribution,
|
|
1800
|
+
totalItems: totalItemCount ?? (portable == null ? null : itemsOrResponse.length),
|
|
706
1801
|
items: itemsOrResponse
|
|
707
1802
|
});
|
|
708
1803
|
} else {
|
|
709
1804
|
const lastCursor = await collectionCallbacks.lastCursor?.(context, identifier);
|
|
710
|
-
const first = new URL(
|
|
1805
|
+
const first = new URL(pageBase);
|
|
711
1806
|
first.searchParams.set("cursor", firstCursor);
|
|
712
1807
|
let last = null;
|
|
713
1808
|
if (lastCursor != null) {
|
|
714
|
-
last = new URL(
|
|
1809
|
+
last = new URL(pageBase);
|
|
715
1810
|
last.searchParams.set("cursor", lastCursor);
|
|
716
1811
|
}
|
|
717
1812
|
collection = new OrderedCollection({
|
|
718
1813
|
id: baseUri,
|
|
1814
|
+
attribution,
|
|
719
1815
|
totalItems: totalItemCount ?? null,
|
|
720
1816
|
first,
|
|
721
1817
|
last
|
|
722
1818
|
});
|
|
723
1819
|
}
|
|
724
1820
|
} else {
|
|
725
|
-
const uri = new URL(baseUri);
|
|
1821
|
+
const uri = new URL(portable == null ? baseUri : pageBase);
|
|
726
1822
|
uri.searchParams.set("cursor", cursor);
|
|
727
|
-
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$
|
|
1823
|
+
const pageOrResponse = await tracer.startActiveSpan(`activitypub.dispatch_collection_page ${name$2}`, {
|
|
728
1824
|
kind: SpanKind.SERVER,
|
|
729
1825
|
attributes: {
|
|
730
1826
|
"activitypub.collection.id": uri.href,
|
|
@@ -740,7 +1836,7 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
740
1836
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
741
1837
|
return await onNotFound(request);
|
|
742
1838
|
}
|
|
743
|
-
const items = filterCollectionItems(page.items, name$
|
|
1839
|
+
const items = filterCollectionItems(page.items, name$2, filterPredicate);
|
|
744
1840
|
itemCount = items.length;
|
|
745
1841
|
span.setAttribute("fedify.collection.items", itemCount);
|
|
746
1842
|
return {
|
|
@@ -762,28 +1858,30 @@ async function handleCollection(request, { name: name$1, identifier, uriGetter,
|
|
|
762
1858
|
const { items, prevCursor, nextCursor } = pageOrResponse;
|
|
763
1859
|
let prev = null;
|
|
764
1860
|
if (prevCursor != null) {
|
|
765
|
-
prev = new URL(
|
|
1861
|
+
prev = new URL(pageBase);
|
|
766
1862
|
prev.searchParams.set("cursor", prevCursor);
|
|
767
1863
|
}
|
|
768
1864
|
let next = null;
|
|
769
1865
|
if (nextCursor != null) {
|
|
770
|
-
next = new URL(
|
|
1866
|
+
next = new URL(pageBase);
|
|
771
1867
|
next.searchParams.set("cursor", nextCursor);
|
|
772
1868
|
}
|
|
773
|
-
const partOf = new URL(
|
|
1869
|
+
const partOf = new URL(pageBase);
|
|
774
1870
|
partOf.searchParams.delete("cursor");
|
|
775
1871
|
collection = new OrderedCollectionPage({
|
|
776
1872
|
id: uri,
|
|
1873
|
+
attribution,
|
|
777
1874
|
prev,
|
|
778
1875
|
next,
|
|
779
1876
|
items,
|
|
780
1877
|
partOf
|
|
781
1878
|
});
|
|
782
1879
|
}
|
|
783
|
-
if (collectionCallbacks.authorizePredicate != null) {
|
|
1880
|
+
if (portable == null && collectionCallbacks.authorizePredicate != null) {
|
|
784
1881
|
if (!await collectionCallbacks.authorizePredicate(context, identifier)) return finish(await onUnauthorized(request), "unauthorized");
|
|
785
1882
|
}
|
|
786
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");
|
|
787
1885
|
return finish(new Response(JSON.stringify(jsonLd), { headers: {
|
|
788
1886
|
"Content-Type": "application/activity+json",
|
|
789
1887
|
Vary: "Accept"
|
|
@@ -950,7 +2048,7 @@ async function handleOutbox(request, { identifier, context: ctx, outboxContextFa
|
|
|
950
2048
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
951
2049
|
});
|
|
952
2050
|
}
|
|
953
|
-
if (!activity.actorIds.every((actorId) => actorId
|
|
2051
|
+
if (!activity.actorIds.every((actorId) => isSameObjectId(actorId, expectedActorId))) {
|
|
954
2052
|
const error = /* @__PURE__ */ new Error("The activity actor does not match the outbox owner.");
|
|
955
2053
|
logger.error("The posted activity actor does not match outbox owner {identifier}.", {
|
|
956
2054
|
identifier,
|
|
@@ -1219,6 +2317,16 @@ async function handleMediaUpload(request, { identifier, context: ctx, mediaUploa
|
|
|
1219
2317
|
* @returns A promise that resolves to an HTTP response.
|
|
1220
2318
|
*/
|
|
1221
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
|
+
}
|
|
1222
2330
|
return await (options.tracerProvider ?? trace.getTracerProvider()).getTracer(name, version).startActiveSpan("activitypub.inbox", {
|
|
1223
2331
|
kind: options.queue == null ? SpanKind.SERVER : SpanKind.PRODUCER,
|
|
1224
2332
|
attributes: { "activitypub.shared_inbox": options.recipient == null }
|
|
@@ -1227,12 +2335,14 @@ async function handleInbox(request, options) {
|
|
|
1227
2335
|
try {
|
|
1228
2336
|
return await handleInboxInternal(request, options, span);
|
|
1229
2337
|
} catch (e) {
|
|
2338
|
+
options.observation.hasException = true;
|
|
1230
2339
|
span.setStatus({
|
|
1231
2340
|
code: SpanStatusCode.ERROR,
|
|
1232
2341
|
message: String(e)
|
|
1233
2342
|
});
|
|
1234
2343
|
throw e;
|
|
1235
2344
|
} finally {
|
|
2345
|
+
options.observation.project(span);
|
|
1236
2346
|
span.end();
|
|
1237
2347
|
}
|
|
1238
2348
|
});
|
|
@@ -1246,7 +2356,9 @@ async function handleInbox(request, options) {
|
|
|
1246
2356
|
* @returns A promise that resolves to an HTTP response.
|
|
1247
2357
|
*/
|
|
1248
2358
|
async function handleInboxInternal(request, parameters, span) {
|
|
1249
|
-
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 };
|
|
1250
2362
|
const logger = getLogger([
|
|
1251
2363
|
"fedify",
|
|
1252
2364
|
"federation",
|
|
@@ -1259,7 +2371,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1259
2371
|
message: "Actor dispatcher is not set."
|
|
1260
2372
|
});
|
|
1261
2373
|
return await onNotFound(request);
|
|
1262
|
-
} else if (recipient != null) {
|
|
2374
|
+
} else if (recipient != null && portableInbox == null) {
|
|
1263
2375
|
const actor = await actorDispatcher(ctx, recipient);
|
|
1264
2376
|
if (actor == null || actor instanceof Tombstone) {
|
|
1265
2377
|
logger.error("Actor {recipient} not found.", { recipient });
|
|
@@ -1271,6 +2383,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1271
2383
|
}
|
|
1272
2384
|
}
|
|
1273
2385
|
if (request.bodyUsed) {
|
|
2386
|
+
observation.result = {
|
|
2387
|
+
disposition: "failed",
|
|
2388
|
+
reason: "bodyUnavailable"
|
|
2389
|
+
};
|
|
1274
2390
|
logger.error("Request body has already been read.", { recipient });
|
|
1275
2391
|
span.setStatus({
|
|
1276
2392
|
code: SpanStatusCode.ERROR,
|
|
@@ -1281,6 +2397,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1281
2397
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1282
2398
|
});
|
|
1283
2399
|
} else if (request.body?.locked) {
|
|
2400
|
+
observation.result = {
|
|
2401
|
+
disposition: "failed",
|
|
2402
|
+
reason: "bodyUnavailable"
|
|
2403
|
+
};
|
|
1284
2404
|
logger.error("Request body is locked.", { recipient });
|
|
1285
2405
|
span.setStatus({
|
|
1286
2406
|
code: SpanStatusCode.ERROR,
|
|
@@ -1291,10 +2411,27 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1291
2411
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1292
2412
|
});
|
|
1293
2413
|
}
|
|
2414
|
+
observation.stage = "parse";
|
|
2415
|
+
observation.result = {
|
|
2416
|
+
disposition: "rejected",
|
|
2417
|
+
reason: "invalidJson"
|
|
2418
|
+
};
|
|
1294
2419
|
let json;
|
|
1295
2420
|
try {
|
|
1296
|
-
json = await request.clone().
|
|
2421
|
+
json = JSON.parse(await readBoundedText(request.clone(), MAX_BODY_SIZE, request.url));
|
|
1297
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
|
+
}
|
|
1298
2435
|
logger.error("Failed to parse JSON:\n{error}", {
|
|
1299
2436
|
recipient,
|
|
1300
2437
|
error
|
|
@@ -1317,7 +2454,21 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1317
2454
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1318
2455
|
});
|
|
1319
2456
|
}
|
|
1320
|
-
|
|
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
|
+
});
|
|
1321
2472
|
const jsonWithoutSig = detachSignature(json);
|
|
1322
2473
|
const hasLdSignature = hasSignature(json);
|
|
1323
2474
|
const canAttemptAlternateAuthAfterLdSignatureFailure = skipSignatureVerification || hasHttpSignatureHeaders(request) || hasObjectIntegrityProof(jsonWithoutSig);
|
|
@@ -1341,6 +2492,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1341
2492
|
code: SpanStatusCode.ERROR,
|
|
1342
2493
|
message: `Failed to parse activity:\n${error}`
|
|
1343
2494
|
});
|
|
2495
|
+
observation.result = {
|
|
2496
|
+
disposition: "rejected",
|
|
2497
|
+
reason: "invalidActivity"
|
|
2498
|
+
};
|
|
1344
2499
|
return new Response("Invalid activity.", {
|
|
1345
2500
|
status: 400,
|
|
1346
2501
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1358,11 +2513,25 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1358
2513
|
recipient,
|
|
1359
2514
|
error
|
|
1360
2515
|
});
|
|
2516
|
+
observation.result = {
|
|
2517
|
+
disposition: "rejected",
|
|
2518
|
+
reason: "invalidJsonLd"
|
|
2519
|
+
};
|
|
1361
2520
|
return new Response("Invalid JSON-LD.", {
|
|
1362
2521
|
status: 400,
|
|
1363
2522
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1364
2523
|
});
|
|
1365
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
|
+
});
|
|
1366
2535
|
if (!canAttemptAlternateAuthAfterLdSignatureFailure) throw error;
|
|
1367
2536
|
if (!skipSignatureVerification) deferredLdSignatureError = error;
|
|
1368
2537
|
logger.debug("Failed to normalize JSON-LD for Linked Data Signatures; deferring to another authentication path only if it verifies:\n{error}", {
|
|
@@ -1374,6 +2543,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1374
2543
|
compactedJsonWithoutSig = detachSignature(compactedJson);
|
|
1375
2544
|
try {
|
|
1376
2545
|
ldSigVerified = await verifyCompactJsonLd(compactedJson, {
|
|
2546
|
+
...signatureObservation,
|
|
1377
2547
|
contextLoader: ctx.contextLoader,
|
|
1378
2548
|
documentLoader: ctx.documentLoader,
|
|
1379
2549
|
keyCache,
|
|
@@ -1387,6 +2557,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1387
2557
|
recipient,
|
|
1388
2558
|
error
|
|
1389
2559
|
});
|
|
2560
|
+
observation.result = {
|
|
2561
|
+
disposition: "rejected",
|
|
2562
|
+
reason: "invalidJsonLd"
|
|
2563
|
+
};
|
|
1390
2564
|
return new Response("Invalid JSON-LD.", {
|
|
1391
2565
|
status: 400,
|
|
1392
2566
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1406,6 +2580,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1406
2580
|
recipient,
|
|
1407
2581
|
error: parseError
|
|
1408
2582
|
});
|
|
2583
|
+
observation.result = {
|
|
2584
|
+
disposition: "rejected",
|
|
2585
|
+
reason: "invalidJsonLd"
|
|
2586
|
+
};
|
|
1409
2587
|
return new Response("Invalid JSON-LD.", {
|
|
1410
2588
|
status: 400,
|
|
1411
2589
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1419,13 +2597,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1419
2597
|
}
|
|
1420
2598
|
let activity = null;
|
|
1421
2599
|
let activityVerified = false;
|
|
2600
|
+
let proofVerified = false;
|
|
1422
2601
|
if (ldSigVerified) {
|
|
1423
2602
|
logger.debug("Linked Data Signatures are verified.", {
|
|
1424
2603
|
recipient,
|
|
1425
2604
|
json
|
|
1426
2605
|
});
|
|
1427
2606
|
try {
|
|
1428
|
-
activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
2607
|
+
activity = observation.activity = await Activity.fromJsonLd(compactedJsonWithoutSig, {
|
|
1429
2608
|
...ctx,
|
|
1430
2609
|
contextLoader: getNormalizationContextLoader(ctx.contextLoader)
|
|
1431
2610
|
});
|
|
@@ -1435,6 +2614,22 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1435
2614
|
return await respondInvalidActivity(error);
|
|
1436
2615
|
}
|
|
1437
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
|
+
}
|
|
1438
2633
|
} else {
|
|
1439
2634
|
logger.debug("Linked Data Signatures are not verified.", {
|
|
1440
2635
|
recipient,
|
|
@@ -1442,11 +2637,13 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1442
2637
|
});
|
|
1443
2638
|
try {
|
|
1444
2639
|
activity = await verifyObject(Activity, jsonWithoutSig, {
|
|
2640
|
+
...signatureObservation,
|
|
1445
2641
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader),
|
|
1446
2642
|
documentLoader: ctx.documentLoader,
|
|
1447
2643
|
keyCache,
|
|
1448
2644
|
meterProvider,
|
|
1449
|
-
tracerProvider
|
|
2645
|
+
tracerProvider,
|
|
2646
|
+
verifyPortableObject: ctx.verifyPortableObject
|
|
1450
2647
|
});
|
|
1451
2648
|
} catch (error) {
|
|
1452
2649
|
if (error instanceof RangeError && await hasMalformedKnownTemporalLiteral(jsonWithoutSig, ctx.contextLoader)) return await respondInvalidActivity(error);
|
|
@@ -1476,6 +2673,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1476
2673
|
code: SpanStatusCode.ERROR,
|
|
1477
2674
|
message: `Failed to parse activity:\n${error}`
|
|
1478
2675
|
});
|
|
2676
|
+
observation.result = {
|
|
2677
|
+
disposition: "rejected",
|
|
2678
|
+
reason: "invalidActivity"
|
|
2679
|
+
};
|
|
1479
2680
|
return new Response("Invalid activity.", {
|
|
1480
2681
|
status: 400,
|
|
1481
2682
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1491,6 +2692,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1491
2692
|
activity: json
|
|
1492
2693
|
});
|
|
1493
2694
|
activityVerified = true;
|
|
2695
|
+
proofVerified = true;
|
|
1494
2696
|
}
|
|
1495
2697
|
}
|
|
1496
2698
|
let httpSigKey = null;
|
|
@@ -1498,15 +2700,25 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1498
2700
|
if (activity == null) {
|
|
1499
2701
|
if (!skipSignatureVerification) {
|
|
1500
2702
|
const verification = await verifyRequestDetailed(request, {
|
|
2703
|
+
...signatureObservation,
|
|
1501
2704
|
contextLoader: ctx.contextLoader,
|
|
1502
2705
|
documentLoader: ctx.documentLoader,
|
|
1503
2706
|
timeWindow: signatureTimeWindow,
|
|
2707
|
+
maxSignatures: maxHttpSignatures,
|
|
1504
2708
|
keyCache,
|
|
1505
2709
|
meterProvider,
|
|
1506
2710
|
tracerProvider
|
|
1507
2711
|
});
|
|
1508
2712
|
if (verification.verified === false) {
|
|
1509
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
|
+
};
|
|
1510
2722
|
const reason = verification.reason;
|
|
1511
2723
|
const remoteHost = "keyId" in reason && reason.keyId != null ? getRemoteHost(reason.keyId) : void 0;
|
|
1512
2724
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure(reason.type, remoteHost);
|
|
@@ -1521,7 +2733,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1521
2733
|
});
|
|
1522
2734
|
if (unverifiedActivityHandler == null) return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1523
2735
|
try {
|
|
1524
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
|
2736
|
+
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, ctx);
|
|
1525
2737
|
} catch (error) {
|
|
1526
2738
|
logger.error("Failed to parse activity:\n{error}", {
|
|
1527
2739
|
recipient,
|
|
@@ -1537,6 +2749,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1537
2749
|
recipient
|
|
1538
2750
|
});
|
|
1539
2751
|
}
|
|
2752
|
+
observation.result = {
|
|
2753
|
+
disposition: "rejected",
|
|
2754
|
+
reason: "invalidActivity"
|
|
2755
|
+
};
|
|
1540
2756
|
return new Response("Invalid activity.", {
|
|
1541
2757
|
status: 400,
|
|
1542
2758
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
@@ -1559,6 +2775,11 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1559
2775
|
try {
|
|
1560
2776
|
response = await unverifiedActivityHandler(ctx, activity, reason);
|
|
1561
2777
|
} catch (error) {
|
|
2778
|
+
observation.result = {
|
|
2779
|
+
disposition: "failed",
|
|
2780
|
+
reason: "listenerError",
|
|
2781
|
+
error
|
|
2782
|
+
};
|
|
1562
2783
|
logger.error("An unexpected error occurred in unverified activity handler:\n{error}", {
|
|
1563
2784
|
error,
|
|
1564
2785
|
activity: json,
|
|
@@ -1575,7 +2796,10 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1575
2796
|
}
|
|
1576
2797
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1577
2798
|
}
|
|
1578
|
-
if (response instanceof Response)
|
|
2799
|
+
if (response instanceof Response) {
|
|
2800
|
+
observation.result = { disposition: "customResponse" };
|
|
2801
|
+
return response;
|
|
2802
|
+
}
|
|
1579
2803
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1580
2804
|
} else {
|
|
1581
2805
|
if (inboxChallengePolicy?.enabled && inboxChallengePolicy.requestNonce) pendingNonceLabel = verification.signatureLabel;
|
|
@@ -1585,7 +2809,7 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1585
2809
|
httpSigKey = verification.key;
|
|
1586
2810
|
}
|
|
1587
2811
|
try {
|
|
1588
|
-
activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
2812
|
+
activity = observation.activity = await Activity.fromJsonLd(jsonWithoutSig, {
|
|
1589
2813
|
...ctx,
|
|
1590
2814
|
contextLoader: wrapContextLoaderForJsonLd(ctx.contextLoader)
|
|
1591
2815
|
});
|
|
@@ -1594,6 +2818,12 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1594
2818
|
return await respondInvalidActivity(error);
|
|
1595
2819
|
}
|
|
1596
2820
|
}
|
|
2821
|
+
observation.activity = activity;
|
|
2822
|
+
observation.stage = "policy";
|
|
2823
|
+
observation.result = {
|
|
2824
|
+
disposition: "rejected",
|
|
2825
|
+
reason: "authentication"
|
|
2826
|
+
};
|
|
1597
2827
|
if (activity.id != null) span.setAttribute("activitypub.activity.id", activity.id.href);
|
|
1598
2828
|
span.setAttribute("activitypub.activity.type", getTypeId(activity).href);
|
|
1599
2829
|
span.addEvent("activitypub.activity.received", {
|
|
@@ -1603,8 +2833,83 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1603
2833
|
"http_signatures.verified": httpSigKey != null,
|
|
1604
2834
|
"http_signatures.key_id": httpSigKey?.id?.href ?? ""
|
|
1605
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
|
+
}
|
|
1606
2903
|
if (httpSigKey != null && !await doesActorOwnKey(activity, httpSigKey, ctx)) {
|
|
1607
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
|
+
};
|
|
1608
2913
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("actorKeyMismatch", httpSigKey.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1609
2914
|
logger.error("The signer ({keyId}) and the actor ({actorId}) do not match.", {
|
|
1610
2915
|
activity: json,
|
|
@@ -1621,13 +2926,70 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1621
2926
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1622
2927
|
});
|
|
1623
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
|
+
}
|
|
1624
2977
|
if (pendingNonceLabel != null) {
|
|
1625
2978
|
if (!await verifySignatureNonce(request, kv, kvPrefixes.acceptSignatureNonce, pendingNonceLabel)) {
|
|
2979
|
+
observation.authentication = {
|
|
2980
|
+
status: "rejected",
|
|
2981
|
+
reason: { type: "invalidNonce" }
|
|
2982
|
+
};
|
|
1626
2983
|
getFederationMetrics(parameters.meterProvider).recordSignatureVerificationFailure("invalidNonce", httpSigKey?.id == null ? void 0 : getRemoteHost(httpSigKey.id));
|
|
1627
2984
|
logger.error("Signature nonce verification failed (missing, expired, or replayed).", { recipient });
|
|
1628
2985
|
return await getFailedSignatureResponse(inboxChallengePolicy, kv, kvPrefixes);
|
|
1629
2986
|
}
|
|
1630
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";
|
|
1631
2993
|
const routeResult = await routeActivity({
|
|
1632
2994
|
context: ctx,
|
|
1633
2995
|
json,
|
|
@@ -1639,7 +3001,14 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1639
3001
|
inboxListeners,
|
|
1640
3002
|
inboxContextFactory,
|
|
1641
3003
|
listenerInboxContextFactory: ldSigVerified ? inboxContextFactory[rawInboxContextFactorySymbol] : void 0,
|
|
1642
|
-
inboxErrorHandler,
|
|
3004
|
+
inboxErrorHandler: async (context, error) => {
|
|
3005
|
+
observation.result = {
|
|
3006
|
+
disposition: "failed",
|
|
3007
|
+
reason: "listenerError",
|
|
3008
|
+
error
|
|
3009
|
+
};
|
|
3010
|
+
await inboxErrorHandler?.(context, error);
|
|
3011
|
+
},
|
|
1643
3012
|
kv,
|
|
1644
3013
|
kvPrefixes,
|
|
1645
3014
|
queue,
|
|
@@ -1648,31 +3017,78 @@ async function handleInboxInternal(request, parameters, span) {
|
|
|
1648
3017
|
tracerProvider,
|
|
1649
3018
|
idempotencyStrategy: parameters.idempotencyStrategy
|
|
1650
3019
|
});
|
|
1651
|
-
if (
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
});
|
|
1655
|
-
else if (routeResult === "missingActor") return new Response("Missing actor.", {
|
|
1656
|
-
status: 400,
|
|
1657
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1658
|
-
});
|
|
1659
|
-
else if (routeResult === "enqueued") return new Response("Activity is enqueued.", {
|
|
1660
|
-
status: 202,
|
|
1661
|
-
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1662
|
-
});
|
|
1663
|
-
else if (routeResult === "unsupportedActivity") return new Response("", {
|
|
1664
|
-
status: 202,
|
|
1665
|
-
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
|
|
1666
3023
|
});
|
|
1667
|
-
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.", {
|
|
1668
3075
|
status: 500,
|
|
1669
3076
|
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1670
3077
|
});
|
|
1671
|
-
else
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
+
}
|
|
1675
3085
|
}
|
|
3086
|
+
const PORTABLE_INBOX_FORWARDABLE_RESULTS = /* @__PURE__ */ new Set([
|
|
3087
|
+
"success",
|
|
3088
|
+
"enqueued",
|
|
3089
|
+
"alreadyProcessed",
|
|
3090
|
+
"unsupportedActivity"
|
|
3091
|
+
]);
|
|
1676
3092
|
/**
|
|
1677
3093
|
* Handles a custom collection request.
|
|
1678
3094
|
* @template TItem The type of items in the collection.
|
|
@@ -1697,19 +3113,11 @@ function recordDeferredPendingCollectionMetrics(error, result, response) {
|
|
|
1697
3113
|
pendingCollectionMetricRecorders.delete(error);
|
|
1698
3114
|
recorder?.(result, response);
|
|
1699
3115
|
}
|
|
1700
|
-
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 }) {
|
|
1701
3117
|
verifyDefined(callbacks);
|
|
1702
3118
|
await authIfNeeded(context, values, callbacks);
|
|
1703
3119
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1704
|
-
|
|
1705
|
-
try {
|
|
1706
|
-
const response = await handler.toJsonLd().then(respondAsActivity);
|
|
1707
|
-
handler.recordPendingCollectionMetrics("served", response);
|
|
1708
|
-
return response;
|
|
1709
|
-
} catch (e) {
|
|
1710
|
-
if (!deferPendingCollectionMetrics(e, (result, response) => handler.recordPendingCollectionMetrics(result, response))) handler.recordPendingCollectionMetrics("error");
|
|
1711
|
-
throw e;
|
|
1712
|
-
}
|
|
3120
|
+
return await respondWithCustomCollection(request, new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate, portable).fetchCollection(cursor), context, portable);
|
|
1713
3121
|
}
|
|
1714
3122
|
/**
|
|
1715
3123
|
* Handles an ordered collection request.
|
|
@@ -1723,13 +3131,19 @@ async function _handleCustomCollection(request, { name, values, context, tracerP
|
|
|
1723
3131
|
* @since 1.8.0
|
|
1724
3132
|
*/
|
|
1725
3133
|
const handleOrderedCollection = exceptWrapper(_handleOrderedCollection);
|
|
1726
|
-
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 }) {
|
|
1727
3135
|
verifyDefined(callbacks);
|
|
1728
3136
|
await authIfNeeded(context, values, callbacks);
|
|
1729
3137
|
const cursor = new URL(request.url).searchParams.get("cursor");
|
|
1730
|
-
|
|
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) {
|
|
1731
3141
|
try {
|
|
1732
|
-
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();
|
|
1733
3147
|
handler.recordPendingCollectionMetrics("served", response);
|
|
1734
3148
|
return response;
|
|
1735
3149
|
} catch (e) {
|
|
@@ -1759,6 +3173,7 @@ var CustomCollectionHandler = class {
|
|
|
1759
3173
|
Collection;
|
|
1760
3174
|
CollectionPage;
|
|
1761
3175
|
filterPredicate;
|
|
3176
|
+
portable;
|
|
1762
3177
|
/**
|
|
1763
3178
|
* The tracer for telemetry.
|
|
1764
3179
|
* @type {Tracer}
|
|
@@ -1795,8 +3210,8 @@ var CustomCollectionHandler = class {
|
|
|
1795
3210
|
* @param CollectionPage The CollectionPage constructor.
|
|
1796
3211
|
* @param filterPredicate Optional filter predicate for items.
|
|
1797
3212
|
*/
|
|
1798
|
-
constructor(name$
|
|
1799
|
-
this.name = name$
|
|
3213
|
+
constructor(name$1, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate, portable) {
|
|
3214
|
+
this.name = name$1;
|
|
1800
3215
|
this.values = values;
|
|
1801
3216
|
this.context = context;
|
|
1802
3217
|
this.callbacks = callbacks;
|
|
@@ -1805,9 +3220,14 @@ var CustomCollectionHandler = class {
|
|
|
1805
3220
|
this.Collection = Collection;
|
|
1806
3221
|
this.CollectionPage = CollectionPage;
|
|
1807
3222
|
this.filterPredicate = filterPredicate;
|
|
3223
|
+
this.portable = portable;
|
|
1808
3224
|
this.name = this.name.trim().replace(/\s+/g, "_");
|
|
1809
3225
|
this.#tracer = this.tracerProvider.getTracer(name, version);
|
|
1810
|
-
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
|
+
}
|
|
1811
3231
|
this.#dispatcher = callbacks.dispatcher.bind(callbacks);
|
|
1812
3232
|
}
|
|
1813
3233
|
/**
|
|
@@ -1858,6 +3278,7 @@ var CustomCollectionHandler = class {
|
|
|
1858
3278
|
this.recordPendingCollectionItemCount(true, items.length);
|
|
1859
3279
|
return {
|
|
1860
3280
|
id,
|
|
3281
|
+
attribution: this.portable?.attribution ?? null,
|
|
1861
3282
|
partOf,
|
|
1862
3283
|
items,
|
|
1863
3284
|
prev: this.appendToUrl(prevCursor),
|
|
@@ -1879,6 +3300,7 @@ var CustomCollectionHandler = class {
|
|
|
1879
3300
|
});
|
|
1880
3301
|
return {
|
|
1881
3302
|
id: this.#id,
|
|
3303
|
+
attribution: this.portable?.attribution ?? null,
|
|
1882
3304
|
first: this.appendToUrl(firstCursor),
|
|
1883
3305
|
last: this.appendToUrl(lastCursor),
|
|
1884
3306
|
totalItems
|
|
@@ -1895,6 +3317,7 @@ var CustomCollectionHandler = class {
|
|
|
1895
3317
|
this.recordPendingCollectionItemCount(false, items.length);
|
|
1896
3318
|
return {
|
|
1897
3319
|
id: this.#id,
|
|
3320
|
+
attribution: this.portable?.attribution ?? null,
|
|
1898
3321
|
totalItems,
|
|
1899
3322
|
items
|
|
1900
3323
|
};
|
|
@@ -2012,7 +3435,7 @@ var CustomCollectionHandler = class {
|
|
|
2012
3435
|
* @returns The URL with cursor appended, or null if cursor is null/undefined.
|
|
2013
3436
|
*/
|
|
2014
3437
|
appendToUrl(cursor) {
|
|
2015
|
-
return appendCursorIfExists(this.context.url, cursor);
|
|
3438
|
+
return appendCursorIfExists(this.portable?.view ?? this.context.url, cursor);
|
|
2016
3439
|
}
|
|
2017
3440
|
/**
|
|
2018
3441
|
* Gets the stored collection or collection page.
|
|
@@ -2089,6 +3512,12 @@ function exceptWrapper(handler) {
|
|
|
2089
3512
|
recordCollectionRequest(meterProvider, collectionAttributes(metricBase, "not_found", response));
|
|
2090
3513
|
return response;
|
|
2091
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
|
+
}
|
|
2092
3521
|
case UnauthorizedError: {
|
|
2093
3522
|
const response = await onUnauthorized(request);
|
|
2094
3523
|
recordDeferredPendingCollectionMetrics(error, "unauthorized", response);
|
|
@@ -2185,6 +3614,16 @@ var UnauthorizedError = class extends HandlerError {
|
|
|
2185
3614
|
}
|
|
2186
3615
|
};
|
|
2187
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
|
+
/**
|
|
2188
3627
|
* Responds with the given object in JSON-LD format.
|
|
2189
3628
|
*
|
|
2190
3629
|
* @param object The object to respond with.
|
|
@@ -2211,7 +3650,7 @@ async function respondWithObjectIfAcceptable(object, request, options) {
|
|
|
2211
3650
|
return response;
|
|
2212
3651
|
}
|
|
2213
3652
|
function generateNonce() {
|
|
2214
|
-
const bytes = new Uint8Array(16);
|
|
3653
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
2215
3654
|
crypto.getRandomValues(bytes);
|
|
2216
3655
|
return btoa(String.fromCharCode(...bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2217
3656
|
}
|
|
@@ -2264,22 +3703,310 @@ async function buildAcceptSignatureHeader(policy, kv, noncePrefix) {
|
|
|
2264
3703
|
parameters
|
|
2265
3704
|
}]);
|
|
2266
3705
|
}
|
|
2267
|
-
async function setKey(kv, key, policy) {
|
|
2268
|
-
const seconds = policy.nonceTtlSeconds ?? 300;
|
|
2269
|
-
const ttl = Temporal.Duration.from({ seconds });
|
|
2270
|
-
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().`);
|
|
2271
4009
|
}
|
|
2272
|
-
const DEF_COMPONENTS = [
|
|
2273
|
-
"@method",
|
|
2274
|
-
"@target-uri",
|
|
2275
|
-
"@authority",
|
|
2276
|
-
"content-digest"
|
|
2277
|
-
];
|
|
2278
|
-
const MIN_COMPONENTS = [
|
|
2279
|
-
"@method",
|
|
2280
|
-
"@target-uri",
|
|
2281
|
-
"@authority"
|
|
2282
|
-
];
|
|
2283
4010
|
//#endregion
|
|
2284
4011
|
//#region src/federation/tasks/enqueue.ts
|
|
2285
4012
|
/**
|
|
@@ -2480,6 +4207,73 @@ const logger = getLogger([
|
|
|
2480
4207
|
"webfinger",
|
|
2481
4208
|
"server"
|
|
2482
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
|
+
}
|
|
2483
4277
|
/**
|
|
2484
4278
|
* Handles a WebFinger request. You would not typically call this function
|
|
2485
4279
|
* directly, but instead use {@link Federation.fetch} method.
|
|
@@ -2528,7 +4322,7 @@ async function handleWebFinger(request, options) {
|
|
|
2528
4322
|
});
|
|
2529
4323
|
}
|
|
2530
4324
|
}
|
|
2531
|
-
const WEBFINGER_HANDLE_SCHEME_WHITELIST = new Set([
|
|
4325
|
+
const WEBFINGER_HANDLE_SCHEME_WHITELIST = /* @__PURE__ */ new Set([
|
|
2532
4326
|
"acct",
|
|
2533
4327
|
"http",
|
|
2534
4328
|
"https",
|
|
@@ -2565,7 +4359,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2565
4359
|
span?.setAttribute("webfinger.resource", resource);
|
|
2566
4360
|
let resourceUrl;
|
|
2567
4361
|
try {
|
|
2568
|
-
resourceUrl =
|
|
4362
|
+
resourceUrl = parseIri(resource);
|
|
2569
4363
|
} catch (e) {
|
|
2570
4364
|
if (e instanceof TypeError) return new Response("Invalid resource URL.", { status: 400 });
|
|
2571
4365
|
throw e;
|
|
@@ -2584,6 +4378,7 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2584
4378
|
return identifier;
|
|
2585
4379
|
}
|
|
2586
4380
|
let identifier = null;
|
|
4381
|
+
let acctUsername = null;
|
|
2587
4382
|
const uriParsed = context.parseUri(resourceUrl);
|
|
2588
4383
|
if (uriParsed?.type != "actor") {
|
|
2589
4384
|
const match = /^acct:([^@]+)@([^@]+)$/.exec(resource);
|
|
@@ -2597,8 +4392,9 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2597
4392
|
const normalizedHost = portMatch == null ? domainToASCII(match[2].toLowerCase()) : domainToASCII(match[2].substring(0, portMatch.index).toLowerCase()) + portMatch[0];
|
|
2598
4393
|
if (normalizedHost != context.url.host && normalizedHost != host) return await onNotFound(request);
|
|
2599
4394
|
else {
|
|
2600
|
-
|
|
2601
|
-
|
|
4395
|
+
acctUsername = match[1];
|
|
4396
|
+
identifier = await mapUsernameToIdentifier(acctUsername);
|
|
4397
|
+
resourceUrl = new URL(`acct:${acctUsername}@${normalizedHost}`);
|
|
2602
4398
|
}
|
|
2603
4399
|
}
|
|
2604
4400
|
} else identifier = uriParsed.identifier;
|
|
@@ -2612,9 +4408,34 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2612
4408
|
status: 410,
|
|
2613
4409
|
headers: { "Access-Control-Allow-Origin": "*" }
|
|
2614
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
|
+
}
|
|
2615
4436
|
const links = [{
|
|
2616
4437
|
rel: "self",
|
|
2617
|
-
href:
|
|
4438
|
+
href: actorUri.href,
|
|
2618
4439
|
type: "application/activity+json"
|
|
2619
4440
|
}];
|
|
2620
4441
|
for (const url of actor.urls) if (url instanceof Link && url.href != null) links.push({
|
|
@@ -2638,23 +4459,25 @@ async function handleWebFingerInternal(request, { context, host, actorDispatcher
|
|
|
2638
4459
|
const customLinks = await webFingerLinksDispatcher(context, resourceUrl);
|
|
2639
4460
|
if (customLinks != null) for (const link of customLinks) links.push(link);
|
|
2640
4461
|
}
|
|
2641
|
-
const aliases =
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
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,
|
|
2654
4478
|
aliases,
|
|
2655
4479
|
links
|
|
2656
|
-
}
|
|
2657
|
-
return new Response(JSON.stringify(jrd), { headers: {
|
|
4480
|
+
}), { headers: {
|
|
2658
4481
|
"Content-Type": "application/jrd+json",
|
|
2659
4482
|
"Access-Control-Allow-Origin": "*"
|
|
2660
4483
|
} });
|
|
@@ -2742,6 +4565,37 @@ function getBenchmarkTriggerOptions(benchmarkOptions) {
|
|
|
2742
4565
|
function maxDelay(first, second) {
|
|
2743
4566
|
return Temporal.Duration.compare(first, second) >= 0 ? first : second;
|
|
2744
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
|
+
}
|
|
2745
4599
|
function isTransportDeliveryError(error) {
|
|
2746
4600
|
return error instanceof FetchError || isAbortError(error);
|
|
2747
4601
|
}
|
|
@@ -2771,7 +4625,7 @@ function isPermanentRemoteContextError(error) {
|
|
|
2771
4625
|
return isRemoteContextLoadingFailure(error) && typeof details?.url === "string" && !URL.canParse(details.url) && isClearlyMalformedContextReference(details.url);
|
|
2772
4626
|
}
|
|
2773
4627
|
function isPermanentInboxParseError(error) {
|
|
2774
|
-
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));
|
|
2775
4629
|
}
|
|
2776
4630
|
function hasLinkedDataSignatureJsonLdProcessingError(error) {
|
|
2777
4631
|
if (error.message.startsWith("Maximum deep iterations exceeded")) return true;
|
|
@@ -2788,6 +4642,8 @@ function isLinkedDataSignatureJsonLdProcessingError(error) {
|
|
|
2788
4642
|
* @returns A new {@link Federation} instance.
|
|
2789
4643
|
* @throws {TypeError} If benchmark mode and `meterProvider` are both
|
|
2790
4644
|
* specified.
|
|
4645
|
+
* @throws {RangeError} If {@link FederationOptions.maxHttpSignatures} is not
|
|
4646
|
+
* a positive integer or `Infinity`.
|
|
2791
4647
|
* @since 0.10.0
|
|
2792
4648
|
*/
|
|
2793
4649
|
function createFederation(options) {
|
|
@@ -2796,6 +4652,9 @@ function createFederation(options) {
|
|
|
2796
4652
|
var FederationImpl = class extends FederationBuilderImpl {
|
|
2797
4653
|
kv;
|
|
2798
4654
|
kvPrefixes;
|
|
4655
|
+
publicKeyTtl;
|
|
4656
|
+
httpMessageSignaturesSpecTtl;
|
|
4657
|
+
portableInboxForwarding;
|
|
2799
4658
|
inboxQueue;
|
|
2800
4659
|
outboxQueue;
|
|
2801
4660
|
fanoutQueue;
|
|
@@ -2811,6 +4670,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2811
4670
|
onOutboxError;
|
|
2812
4671
|
permanentFailureStatusCodes;
|
|
2813
4672
|
signatureTimeWindow;
|
|
4673
|
+
maxHttpSignatures;
|
|
2814
4674
|
skipSignatureVerification;
|
|
2815
4675
|
outboxRetryPolicy;
|
|
2816
4676
|
inboxRetryPolicy;
|
|
@@ -2828,6 +4688,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2828
4688
|
benchmarkMetricReader;
|
|
2829
4689
|
benchmarkTriggerOptions;
|
|
2830
4690
|
#mediaUploaderNoAuthWarned = false;
|
|
4691
|
+
#portableInboxForwardingWarned = false;
|
|
2831
4692
|
#queueDepthGaugeSourceId = `fedify-${(++nextQueueDepthGaugeSourceId).toString(36)}`;
|
|
2832
4693
|
#queueDepthGaugeEntries = [];
|
|
2833
4694
|
#queueDepthGaugeMeterProvider;
|
|
@@ -2838,6 +4699,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2838
4699
|
const hasCustomLoaderFactory = options.documentLoaderFactory != null || options.contextLoaderFactory != null;
|
|
2839
4700
|
const allowPrivateAddress = options.allowPrivateAddress ?? (benchmarkMode && !hasCustomLoaderFactory ? true : false);
|
|
2840
4701
|
const signatureTimeWindow = options.signatureTimeWindow ?? (benchmarkMode ? false : { hours: 1 });
|
|
4702
|
+
if (options.maxHttpSignatures !== void 0) validateMaxSignatures(options.maxHttpSignatures, "maxHttpSignatures");
|
|
2841
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.");
|
|
2842
4704
|
if (benchmarkMode) {
|
|
2843
4705
|
const relaxations = getBenchmarkRelaxations(allowPrivateAddress, signatureTimeWindow);
|
|
@@ -2859,8 +4721,12 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2859
4721
|
acceptSignatureNonce: ["_fedify", "acceptSignatureNonce"],
|
|
2860
4722
|
circuitBreaker: ["_fedify", "circuit"],
|
|
2861
4723
|
taskDeduplication: ["_fedify", "taskDeduplication"],
|
|
4724
|
+
portableInboxForwarding: ["_fedify", "portableInboxForwarding"],
|
|
2862
4725
|
...options.kvPrefixes ?? {}
|
|
2863
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);
|
|
2864
4730
|
if (options.queue == null) {
|
|
2865
4731
|
this.inboxQueue = void 0;
|
|
2866
4732
|
this.outboxQueue = void 0;
|
|
@@ -2926,13 +4792,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2926
4792
|
}
|
|
2927
4793
|
const { userAgent } = options;
|
|
2928
4794
|
this.allowPrivateAddress = allowPrivateAddress;
|
|
4795
|
+
const documentLoaderTimeout = resolveDocumentLoaderTimeout(options.documentLoaderTimeout == null ? options.documentLoaderTimeout : Temporal.Duration.from(options.documentLoaderTimeout).total("millisecond"));
|
|
2929
4796
|
const userDocumentLoaderFactory = options.documentLoaderFactory;
|
|
2930
4797
|
const userContextLoaderFactory = options.contextLoaderFactory;
|
|
2931
4798
|
const userAuthFactory = options.authenticatedDocumentLoaderFactory;
|
|
2932
4799
|
const builtinDocumentLoaderFactory = (opts) => kvCache({
|
|
2933
4800
|
loader: getDocumentLoader({
|
|
2934
4801
|
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2935
|
-
userAgent: opts?.userAgent ?? userAgent
|
|
4802
|
+
userAgent: opts?.userAgent ?? userAgent,
|
|
4803
|
+
timeout: opts?.timeout !== void 0 ? opts.timeout : documentLoaderTimeout
|
|
2936
4804
|
}),
|
|
2937
4805
|
kv: options.kv,
|
|
2938
4806
|
prefix: this.kvPrefixes.remoteDocument,
|
|
@@ -2942,7 +4810,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2942
4810
|
const builtinContextLoaderFactory = (opts) => kvCache({
|
|
2943
4811
|
loader: getDocumentLoader({
|
|
2944
4812
|
allowPrivateAddress: opts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2945
|
-
userAgent: opts?.userAgent ?? userAgent
|
|
4813
|
+
userAgent: opts?.userAgent ?? userAgent,
|
|
4814
|
+
timeout: opts?.timeout !== void 0 ? opts.timeout : documentLoaderTimeout
|
|
2946
4815
|
}),
|
|
2947
4816
|
kv: options.kv,
|
|
2948
4817
|
prefix: this.kvPrefixes.remoteDocument,
|
|
@@ -2963,7 +4832,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2963
4832
|
this.authenticatedDocumentLoaderFactory = (identity, factoryOpts) => instrumentDocumentLoader(userAuthFactory != null ? userAuthFactory(identity, factoryOpts) : getAuthenticatedDocumentLoader(identity, {
|
|
2964
4833
|
allowPrivateAddress: factoryOpts?.allowPrivateAddress ?? allowPrivateAddress,
|
|
2965
4834
|
userAgent: factoryOpts?.userAgent ?? userAgent,
|
|
2966
|
-
|
|
4835
|
+
timeout: factoryOpts?.timeout !== void 0 ? factoryOpts.timeout : documentLoaderTimeout,
|
|
4836
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
2967
4837
|
tracerProvider: this.tracerProvider
|
|
2968
4838
|
}), {
|
|
2969
4839
|
meterProvider: this._meterProvider,
|
|
@@ -2974,6 +4844,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
2974
4844
|
this.onOutboxError = options.onOutboxError;
|
|
2975
4845
|
this.permanentFailureStatusCodes = options.permanentFailureStatusCodes ?? [404, 410];
|
|
2976
4846
|
this.signatureTimeWindow = signatureTimeWindow;
|
|
4847
|
+
this.maxHttpSignatures = options.maxHttpSignatures ?? 3;
|
|
2977
4848
|
this.skipSignatureVerification = options.skipSignatureVerification ?? false;
|
|
2978
4849
|
this.inboxChallengePolicy = options.inboxChallengePolicy;
|
|
2979
4850
|
this.outboxRetryPolicy = options.outboxRetryPolicy ?? createExponentialBackoffPolicy();
|
|
@@ -3206,6 +5077,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3206
5077
|
collectionSync: message.collectionSync,
|
|
3207
5078
|
orderingKey: message.orderingKey,
|
|
3208
5079
|
normalizeExistingProofs: message.normalizeExistingProofs,
|
|
5080
|
+
activityJsonLd: message.activity,
|
|
3209
5081
|
context
|
|
3210
5082
|
});
|
|
3211
5083
|
}
|
|
@@ -3246,11 +5118,16 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3246
5118
|
});
|
|
3247
5119
|
return parsedActorIds;
|
|
3248
5120
|
};
|
|
3249
|
-
const parseActivity = () =>
|
|
3250
|
-
contextLoader
|
|
3251
|
-
documentLoader
|
|
3252
|
-
|
|
3253
|
-
|
|
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
|
+
};
|
|
3254
5131
|
const enqueueHeldOutboxMessage = async (delay, heldSince) => {
|
|
3255
5132
|
const { outboxQueue } = this;
|
|
3256
5133
|
if (outboxQueue == null) return;
|
|
@@ -3299,6 +5176,20 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3299
5176
|
}
|
|
3300
5177
|
recordOutboxActivity(this.meterProvider, "abandoned", message.activityType);
|
|
3301
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
|
+
}
|
|
3302
5193
|
try {
|
|
3303
5194
|
const inbox = new URL(message.inbox);
|
|
3304
5195
|
const circuit = this.outboxQueue == null ? void 0 : this.circuitBreaker;
|
|
@@ -3331,6 +5222,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3331
5222
|
if (decision.stateChange != null) recordCircuitBreakerSpanEvent(span, remoteHost, decision.stateChange);
|
|
3332
5223
|
}
|
|
3333
5224
|
await sendActivity({
|
|
5225
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
3334
5226
|
keys,
|
|
3335
5227
|
activity: message.activity,
|
|
3336
5228
|
activityId: message.activityId,
|
|
@@ -3338,7 +5230,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3338
5230
|
inbox,
|
|
3339
5231
|
sharedInbox: message.sharedInbox,
|
|
3340
5232
|
headers: new Headers(message.headers),
|
|
3341
|
-
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
5233
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
3342
5234
|
meterProvider: this.meterProvider,
|
|
3343
5235
|
tracerProvider: this.tracerProvider
|
|
3344
5236
|
});
|
|
@@ -3520,6 +5412,308 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3520
5412
|
}
|
|
3521
5413
|
logger.info("Successfully sent activity {activityId} to {inbox}.", { ...logData });
|
|
3522
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
|
+
}
|
|
3523
5717
|
async #listenInboxMessage(ctxData, message, span, onActivityType) {
|
|
3524
5718
|
const logger = getLogger([
|
|
3525
5719
|
"fedify",
|
|
@@ -3528,8 +5722,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3528
5722
|
]);
|
|
3529
5723
|
const baseUrl = new URL(message.baseUrl);
|
|
3530
5724
|
let context = this.#createContext(baseUrl, ctxData);
|
|
3531
|
-
if (message.identifier != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: await context.getDocumentLoader({ identifier: message.identifier }) });
|
|
3532
|
-
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) {
|
|
3533
5727
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
3534
5728
|
if (identity != null) context = this.#createContext(baseUrl, ctxData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
3535
5729
|
}
|
|
@@ -3861,7 +6055,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3861
6055
|
...ctxOptions,
|
|
3862
6056
|
request,
|
|
3863
6057
|
invokedFromActorDispatcher: opts.invokedFromActorDispatcher,
|
|
3864
|
-
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher
|
|
6058
|
+
invokedFromObjectDispatcher: opts.invokedFromObjectDispatcher,
|
|
6059
|
+
portableRequest: opts.portableRequest
|
|
3865
6060
|
});
|
|
3866
6061
|
}
|
|
3867
6062
|
#getLoaderOptions(origin) {
|
|
@@ -3886,36 +6081,33 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3886
6081
|
else if (keys.length < 1) throw new TypeError("The keys must not be empty.");
|
|
3887
6082
|
const contextLoader = this.contextLoaderFactory(this.#getLoaderOptions(ctx.origin));
|
|
3888
6083
|
const activityId = activity.id.href;
|
|
3889
|
-
let hasProof = false;
|
|
3890
|
-
let proofCreated = false;
|
|
3891
6084
|
let rsaKey = null;
|
|
6085
|
+
const ldSignable = !hasPortableActor(activity);
|
|
3892
6086
|
for (const { keyId, privateKey } of keys) {
|
|
3893
6087
|
validateCryptoKey(privateKey, "private");
|
|
3894
|
-
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 = {
|
|
3895
6089
|
keyId,
|
|
3896
6090
|
privateKey
|
|
3897
6091
|
};
|
|
3898
6092
|
}
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
}
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
proofCreated = true;
|
|
3911
|
-
}
|
|
3912
|
-
}
|
|
3913
|
-
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({
|
|
3914
6104
|
format: "compact",
|
|
3915
6105
|
contextLoader
|
|
3916
6106
|
});
|
|
3917
|
-
if (proofCreated || !hasProof || options.normalizeExistingProofs) jsonLd = await normalizeOutgoingActivityJsonLd(jsonLd, contextLoader);
|
|
3918
|
-
|
|
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.", {
|
|
3919
6111
|
activityId,
|
|
3920
6112
|
keys: keys.map((pair) => ({
|
|
3921
6113
|
keyId: pair.keyId.href,
|
|
@@ -3951,15 +6143,15 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3951
6143
|
activity: jsonLd
|
|
3952
6144
|
});
|
|
3953
6145
|
const promises = [];
|
|
3954
|
-
for (const inbox in inboxes) promises.push(
|
|
6146
|
+
for (const inbox in inboxes) promises.push(sendActivityToInbox(inbox, inboxes[inbox].gatewayInboxes, {
|
|
6147
|
+
allowPrivateAddress: this.allowPrivateAddress,
|
|
3955
6148
|
keys,
|
|
3956
6149
|
activity: jsonLd,
|
|
3957
6150
|
activityId: activity.id?.href,
|
|
3958
6151
|
activityType: getTypeId(activity).href,
|
|
3959
|
-
inbox: new URL(inbox),
|
|
3960
6152
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
3961
6153
|
headers: collectionSync == null ? void 0 : new Headers({ "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) }),
|
|
3962
|
-
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock),
|
|
6154
|
+
specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, this.firstKnock, { specTtl: this.httpMessageSignaturesSpecTtl }),
|
|
3963
6155
|
meterProvider: this.meterProvider,
|
|
3964
6156
|
tracerProvider: this.tracerProvider
|
|
3965
6157
|
}));
|
|
@@ -3983,8 +6175,8 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
3983
6175
|
propagation.inject(context.active(), carrier);
|
|
3984
6176
|
const messages = [];
|
|
3985
6177
|
for (const inbox in inboxes) {
|
|
3986
|
-
const
|
|
3987
|
-
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)}`;
|
|
3988
6180
|
const message = {
|
|
3989
6181
|
type: "outbox",
|
|
3990
6182
|
id: crypto.randomUUID(),
|
|
@@ -4000,6 +6192,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4000
6192
|
attempt: 0,
|
|
4001
6193
|
headers: collectionSync == null ? {} : { "Collection-Synchronization": await buildCollectionSynchronizationHeader(collectionSync, inboxes[inbox].actorIds) },
|
|
4002
6194
|
orderingKey: messageOrderingKey,
|
|
6195
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
4003
6196
|
traceContext: carrier
|
|
4004
6197
|
};
|
|
4005
6198
|
messages.push({
|
|
@@ -4050,7 +6243,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4050
6243
|
return await withContext({
|
|
4051
6244
|
traceId: spanCtx.traceId,
|
|
4052
6245
|
spanId: spanCtx.spanId
|
|
4053
|
-
}, async () => {
|
|
6246
|
+
}, () => this.#observeInboxFetch(metricState, async () => {
|
|
4054
6247
|
const logger = getLogger([
|
|
4055
6248
|
"fedify",
|
|
4056
6249
|
"federation",
|
|
@@ -4065,8 +6258,14 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4065
6258
|
tracer,
|
|
4066
6259
|
metricState
|
|
4067
6260
|
});
|
|
4068
|
-
if (
|
|
6261
|
+
if (metricState.inboxCompletion != null) metricState.inboxCompletion.observation.stage = "respond";
|
|
6262
|
+
if (metricState.endpoint !== "hashlink_media" && acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
|
|
4069
6263
|
} catch (error) {
|
|
6264
|
+
metricState.inboxCompletion?.observation.project(span, {
|
|
6265
|
+
type: "exception",
|
|
6266
|
+
stage: metricState.inboxCompletion.observation.stage,
|
|
6267
|
+
error
|
|
6268
|
+
});
|
|
4070
6269
|
this.metrics.recordHttpServerRequest(request.method, metricState.endpoint ?? "error", getDurationMs(metricStart), { routeTemplate: metricState.routeTemplate });
|
|
4071
6270
|
span.setStatus({
|
|
4072
6271
|
code: SpanStatusCode.ERROR,
|
|
@@ -4105,7 +6304,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4105
6304
|
else if (response.status >= 400) logger.warn(logTpl, values);
|
|
4106
6305
|
else logger.info(logTpl, values);
|
|
4107
6306
|
return response;
|
|
4108
|
-
});
|
|
6307
|
+
}));
|
|
4109
6308
|
});
|
|
4110
6309
|
});
|
|
4111
6310
|
}
|
|
@@ -4116,14 +6315,48 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4116
6315
|
const url = new URL(request.url);
|
|
4117
6316
|
const route = this.router.route(url.pathname);
|
|
4118
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
|
+
}));
|
|
4119
6343
|
metricState.endpoint = "not_found";
|
|
4120
6344
|
return await onNotFound(request);
|
|
4121
6345
|
}
|
|
4122
6346
|
metricState.routeTemplate = route.template;
|
|
4123
6347
|
metricState.endpoint = getEndpointCategory(route.name);
|
|
4124
6348
|
span.updateName(`${request.method} ${route.template}`);
|
|
4125
|
-
let context = this.#createContext(request, contextData);
|
|
4126
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
|
+
}
|
|
4127
6360
|
switch (routeName) {
|
|
4128
6361
|
case "webfinger": return await handleWebFinger(request, {
|
|
4129
6362
|
context,
|
|
@@ -4182,6 +6415,28 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4182
6415
|
});
|
|
4183
6416
|
return response;
|
|
4184
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
|
+
}
|
|
4185
6440
|
switch (routeName) {
|
|
4186
6441
|
case "actor":
|
|
4187
6442
|
case "actorAlias": {
|
|
@@ -4257,35 +6512,18 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4257
6512
|
onUnauthorized,
|
|
4258
6513
|
onNotFound
|
|
4259
6514
|
});
|
|
4260
|
-
|
|
4261
|
-
case "sharedInbox":
|
|
4262
|
-
if (
|
|
6515
|
+
throw new Error("Unreachable inbox delivery.");
|
|
6516
|
+
case "sharedInbox":
|
|
6517
|
+
if (this.sharedInboxKeyDispatcher != null) {
|
|
4263
6518
|
const identity = await this.sharedInboxKeyDispatcher(context);
|
|
4264
6519
|
if (identity != null) context = this.#createContext(request, contextData, { documentLoader: "identifier" in identity || "username" in identity ? await context.getDocumentLoader(identity) : context.getDocumentLoader(identity) });
|
|
4265
|
-
}
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
context,
|
|
4272
|
-
inboxContextFactory,
|
|
4273
|
-
kv: this.kv,
|
|
4274
|
-
kvPrefixes: this.kvPrefixes,
|
|
4275
|
-
queue: this.inboxQueue,
|
|
4276
|
-
actorDispatcher: this.actorCallbacks?.dispatcher,
|
|
4277
|
-
inboxListeners: this.inboxListeners,
|
|
4278
|
-
inboxErrorHandler: this.inboxErrorHandler,
|
|
4279
|
-
unverifiedActivityHandler: this.unverifiedActivityHandler,
|
|
4280
|
-
onNotFound,
|
|
4281
|
-
signatureTimeWindow: this.signatureTimeWindow,
|
|
4282
|
-
skipSignatureVerification: this.skipSignatureVerification,
|
|
4283
|
-
inboxChallengePolicy: this.inboxChallengePolicy,
|
|
4284
|
-
meterProvider: this.meterProvider,
|
|
4285
|
-
tracerProvider: this.tracerProvider,
|
|
4286
|
-
idempotencyStrategy: this.idempotencyStrategy
|
|
6520
|
+
}
|
|
6521
|
+
return await this.#handleInbox(request, {
|
|
6522
|
+
recipient: route.values.identifier ?? null,
|
|
6523
|
+
context,
|
|
6524
|
+
contextData,
|
|
6525
|
+
onNotFound
|
|
4287
6526
|
});
|
|
4288
|
-
}
|
|
4289
6527
|
case "following": return await handleCollection(request, {
|
|
4290
6528
|
name: "following",
|
|
4291
6529
|
identifier: route.values.identifier,
|
|
@@ -4298,12 +6536,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4298
6536
|
onNotFound
|
|
4299
6537
|
});
|
|
4300
6538
|
case "followers": {
|
|
4301
|
-
|
|
4302
|
-
if (baseUrl != null) try {
|
|
4303
|
-
baseUrl = `${new URL(baseUrl).origin}/`;
|
|
4304
|
-
} catch {
|
|
4305
|
-
baseUrl = null;
|
|
4306
|
-
}
|
|
6539
|
+
const baseUrl = getFollowersBaseUrl(url);
|
|
4307
6540
|
return await handleCollection(request, {
|
|
4308
6541
|
name: "followers",
|
|
4309
6542
|
identifier: route.values.identifier,
|
|
@@ -4314,7 +6547,7 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4314
6547
|
},
|
|
4315
6548
|
context,
|
|
4316
6549
|
filter: baseUrl != null ? new URL(baseUrl) : void 0,
|
|
4317
|
-
filterPredicate: baseUrl != null ? (
|
|
6550
|
+
filterPredicate: baseUrl != null ? getFollowersFilterPredicate(baseUrl) : void 0,
|
|
4318
6551
|
collectionCallbacks: this.followersCallbacks,
|
|
4319
6552
|
tracerProvider: this.tracerProvider,
|
|
4320
6553
|
meterProvider: this._meterProvider,
|
|
@@ -4356,10 +6589,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4356
6589
|
onNotFound
|
|
4357
6590
|
});
|
|
4358
6591
|
case "collection": {
|
|
4359
|
-
const name = route.name
|
|
6592
|
+
const name = this.getCollectionName(route.name);
|
|
4360
6593
|
const callbacks = this.collectionCallbacks[name];
|
|
4361
6594
|
return await handleCustomCollection(request, {
|
|
4362
|
-
name,
|
|
6595
|
+
name: String(name),
|
|
4363
6596
|
context,
|
|
4364
6597
|
values: route.values,
|
|
4365
6598
|
collectionCallbacks: callbacks,
|
|
@@ -4370,10 +6603,10 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4370
6603
|
});
|
|
4371
6604
|
}
|
|
4372
6605
|
case "orderedCollection": {
|
|
4373
|
-
const name = route.name
|
|
6606
|
+
const name = this.getCollectionName(route.name);
|
|
4374
6607
|
const callbacks = this.collectionCallbacks[name];
|
|
4375
6608
|
return await handleOrderedCollection(request, {
|
|
4376
|
-
name,
|
|
6609
|
+
name: String(name),
|
|
4377
6610
|
context,
|
|
4378
6611
|
values: route.values,
|
|
4379
6612
|
collectionCallbacks: callbacks,
|
|
@@ -4390,7 +6623,499 @@ var FederationImpl = class extends FederationBuilderImpl {
|
|
|
4390
6623
|
}
|
|
4391
6624
|
}
|
|
4392
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."
|
|
4393
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
|
+
}
|
|
4394
7119
|
function getEndpointCategory(routeName) {
|
|
4395
7120
|
if (routeName.startsWith("object:")) return "object";
|
|
4396
7121
|
if (routeName.startsWith("collection:") || routeName.startsWith("orderedCollection:")) return "collection";
|
|
@@ -4438,12 +7163,28 @@ function getCollectionMetricRoute(routeName) {
|
|
|
4438
7163
|
}
|
|
4439
7164
|
}
|
|
4440
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
|
+
}
|
|
4441
7181
|
var ContextImpl = class ContextImpl {
|
|
4442
7182
|
url;
|
|
4443
7183
|
federation;
|
|
4444
7184
|
data;
|
|
4445
7185
|
documentLoader;
|
|
4446
7186
|
contextLoader;
|
|
7187
|
+
verifyPortableObject;
|
|
4447
7188
|
invokedFromActorKeyPairsDispatcher;
|
|
4448
7189
|
#codec;
|
|
4449
7190
|
constructor({ url, federation, data, documentLoader, contextLoader, invokedFromActorKeyPairsDispatcher }) {
|
|
@@ -4452,6 +7193,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4452
7193
|
this.data = data;
|
|
4453
7194
|
this.documentLoader = documentLoader;
|
|
4454
7195
|
this.contextLoader = contextLoader;
|
|
7196
|
+
this.verifyPortableObject = createPortableObjectVerifier(documentLoader, contextLoader, federation);
|
|
4455
7197
|
this.invokedFromActorKeyPairsDispatcher = invokedFromActorKeyPairsDispatcher;
|
|
4456
7198
|
}
|
|
4457
7199
|
/**
|
|
@@ -4520,22 +7262,43 @@ var ContextImpl = class ContextImpl {
|
|
|
4520
7262
|
return new URL(path, this.canonicalOrigin);
|
|
4521
7263
|
}
|
|
4522
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) {
|
|
4523
7272
|
const path = this.federation.router.build(`actorAlias:${identifier}`, {}) ?? this.federation.router.build("actor", { identifier });
|
|
4524
7273
|
if (path == null) throw new RouterError("No actor dispatcher registered.");
|
|
4525
|
-
return
|
|
7274
|
+
return path;
|
|
4526
7275
|
}
|
|
4527
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) {
|
|
4528
7284
|
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
|
4529
7285
|
if (callbacks == null) throw new RouterError("No object dispatcher registered.");
|
|
4530
7286
|
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
|
4531
7287
|
const path = this.federation.router.build(`object:${cls.typeId.href}`, values);
|
|
4532
7288
|
if (path == null) throw new RouterError("No object dispatcher registered.");
|
|
4533
|
-
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;
|
|
4534
7295
|
}
|
|
4535
7296
|
getOutboxUri(identifier) {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
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);
|
|
4539
7302
|
}
|
|
4540
7303
|
getMediaUploaderUri(identifier) {
|
|
4541
7304
|
const path = this.federation.router.build("mediaUploader", { identifier });
|
|
@@ -4552,42 +7315,75 @@ var ContextImpl = class ContextImpl {
|
|
|
4552
7315
|
if (path == null) throw new RouterError("No inbox path registered.");
|
|
4553
7316
|
return new URL(path, this.canonicalOrigin);
|
|
4554
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
|
+
}
|
|
4555
7323
|
getFollowingUri(identifier) {
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
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);
|
|
4559
7329
|
}
|
|
4560
7330
|
getFollowersUri(identifier) {
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
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);
|
|
4564
7336
|
}
|
|
4565
7337
|
getLikedUri(identifier) {
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
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);
|
|
4569
7343
|
}
|
|
4570
7344
|
getFeaturedUri(identifier) {
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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);
|
|
4574
7350
|
}
|
|
4575
7351
|
getFeaturedTagsUri(identifier) {
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
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);
|
|
4579
7357
|
}
|
|
4580
7358
|
getCollectionUri(name, values) {
|
|
4581
7359
|
const path = this.federation.getCollectionPath(name, values);
|
|
4582
7360
|
if (path === null) throw new RouterError(`No collection dispatcher registered for "${String(name)}".`);
|
|
4583
7361
|
return new URL(path, this.canonicalOrigin);
|
|
4584
7362
|
}
|
|
4585
|
-
|
|
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 = {}) {
|
|
4586
7369
|
if (uri == null) return null;
|
|
4587
|
-
if (uri.origin
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
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 {
|
|
4591
7387
|
type: "inbox",
|
|
4592
7388
|
identifier: void 0
|
|
4593
7389
|
};
|
|
@@ -4635,8 +7431,10 @@ var ContextImpl = class ContextImpl {
|
|
|
4635
7431
|
const collectionRegex = new RegExp(`^(${["collection", "orderedCollection"].join("|")}):(.*)$`);
|
|
4636
7432
|
const match = route.name.match(collectionRegex);
|
|
4637
7433
|
if (match !== null) {
|
|
4638
|
-
const [, type
|
|
7434
|
+
const [, type] = match;
|
|
7435
|
+
const name = this.federation.getCollectionName(route.name);
|
|
4639
7436
|
const cls = this.federation.collectionTypeIds[name];
|
|
7437
|
+
if (cls == null) return null;
|
|
4640
7438
|
return {
|
|
4641
7439
|
type,
|
|
4642
7440
|
name,
|
|
@@ -4657,14 +7455,15 @@ var ContextImpl = class ContextImpl {
|
|
|
4657
7455
|
getActorKeyPairsIdentifier: identifier,
|
|
4658
7456
|
actorKeyPairsDispatcherIdentifier: this.invokedFromActorKeyPairsDispatcher.identifier
|
|
4659
7457
|
});
|
|
7458
|
+
const portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
4660
7459
|
let keyPairs;
|
|
4661
7460
|
try {
|
|
4662
|
-
keyPairs = await this.getKeyPairsFromIdentifier(identifier);
|
|
7461
|
+
keyPairs = await this.getKeyPairsFromIdentifier(identifier, portableActor);
|
|
4663
7462
|
} catch (_) {
|
|
4664
7463
|
logger.warn("No actor key pairs dispatcher registered.");
|
|
4665
7464
|
return [];
|
|
4666
7465
|
}
|
|
4667
|
-
const owner = this.getActorUri(identifier);
|
|
7466
|
+
const owner = portableActor?.id ?? this.getActorUri(identifier);
|
|
4668
7467
|
const result = [];
|
|
4669
7468
|
let i = 1;
|
|
4670
7469
|
for (const keyPair of keyPairs) {
|
|
@@ -4676,7 +7475,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4676
7475
|
publicKey: keyPair.publicKey
|
|
4677
7476
|
}),
|
|
4678
7477
|
multikey: new Multikey({
|
|
4679
|
-
id: new URL(`#multikey-${i}`, owner),
|
|
7478
|
+
id: portableActor == null ? new URL(`#multikey-${i}`, owner) : keyPair.keyId,
|
|
4680
7479
|
controller: owner,
|
|
4681
7480
|
publicKey: keyPair.publicKey
|
|
4682
7481
|
})
|
|
@@ -4686,7 +7485,26 @@ var ContextImpl = class ContextImpl {
|
|
|
4686
7485
|
}
|
|
4687
7486
|
return result;
|
|
4688
7487
|
}
|
|
4689
|
-
|
|
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) {
|
|
4690
7508
|
const logger = getLogger([
|
|
4691
7509
|
"fedify",
|
|
4692
7510
|
"federation",
|
|
@@ -4703,6 +7521,8 @@ var ContextImpl = class ContextImpl {
|
|
|
4703
7521
|
}
|
|
4704
7522
|
throw error;
|
|
4705
7523
|
}
|
|
7524
|
+
if (portableActor === void 0) portableActor = await this.getPortableActorKeyOwner(identifier);
|
|
7525
|
+
const keyBase = portableActor?.keyBase ?? actorUri;
|
|
4706
7526
|
const keyPairs = await this.federation.actorCallbacks?.keyPairsDispatcher(new ContextImpl({
|
|
4707
7527
|
...this,
|
|
4708
7528
|
invokedFromActorKeyPairsDispatcher: { identifier }
|
|
@@ -4713,7 +7533,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4713
7533
|
for (const keyPair of keyPairs) {
|
|
4714
7534
|
result.push({
|
|
4715
7535
|
...keyPair,
|
|
4716
|
-
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`,
|
|
7536
|
+
keyId: new URL(i == 0 ? `#main-key` : `#key-${i + 1}`, keyBase)
|
|
4717
7537
|
});
|
|
4718
7538
|
i++;
|
|
4719
7539
|
}
|
|
@@ -4759,6 +7579,7 @@ var ContextImpl = class ContextImpl {
|
|
|
4759
7579
|
userAgent: options.userAgent ?? this.federation.userAgent,
|
|
4760
7580
|
tracerProvider: options.tracerProvider ?? this.tracerProvider,
|
|
4761
7581
|
meterProvider: options.meterProvider ?? this.meterProvider,
|
|
7582
|
+
verifyPortableObject: options.verifyPortableObject ?? this.verifyPortableObject,
|
|
4762
7583
|
allowPrivateAddress: this.federation.allowPrivateAddress
|
|
4763
7584
|
});
|
|
4764
7585
|
}
|
|
@@ -4766,7 +7587,8 @@ var ContextImpl = class ContextImpl {
|
|
|
4766
7587
|
return traverseCollection(collection, {
|
|
4767
7588
|
...options,
|
|
4768
7589
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
4769
|
-
contextLoader: options.contextLoader ?? this.contextLoader
|
|
7590
|
+
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
7591
|
+
verifyPortableObject: options.verifyPortableObject ?? this.verifyPortableObject
|
|
4770
7592
|
});
|
|
4771
7593
|
}
|
|
4772
7594
|
lookupNodeInfo(url, options = {}) {
|
|
@@ -4884,18 +7706,18 @@ var ContextImpl = class ContextImpl {
|
|
|
4884
7706
|
});
|
|
4885
7707
|
throw new TypeError("The activity to send must have at least one actor property.");
|
|
4886
7708
|
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
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;
|
|
4899
7721
|
const inboxes = extractInboxes({
|
|
4900
7722
|
recipients: expandedRecipients,
|
|
4901
7723
|
preferSharedInbox: options.preferSharedInbox,
|
|
@@ -4913,13 +7735,27 @@ var ContextImpl = class ContextImpl {
|
|
|
4913
7735
|
activityId: activity.id?.href,
|
|
4914
7736
|
activity
|
|
4915
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
|
+
}
|
|
4916
7750
|
if (this.federation.fanoutQueue == null || options.immediate || options.fanout === "skip" || (options.fanout ?? "auto") === "auto" && globalThis.Object.keys(inboxes).length < FANOUT_THRESHOLD) {
|
|
4917
7751
|
await this.federation.sendActivity(keys, inboxes, activity, {
|
|
4918
7752
|
...opts,
|
|
7753
|
+
activityJsonLd,
|
|
4919
7754
|
normalizeExistingProofs: proofCreated || options.normalizeExistingProofs
|
|
4920
7755
|
});
|
|
4921
7756
|
return true;
|
|
4922
7757
|
}
|
|
7758
|
+
assertSupportedCompoundProofShape(activityJsonLd, activity.id?.href);
|
|
4923
7759
|
const keyJwkPairs = await Promise.all(keys.map(async ({ keyId, privateKey }) => ({
|
|
4924
7760
|
keyId: keyId.href,
|
|
4925
7761
|
privateKey: await exportJwk(privateKey)
|
|
@@ -4931,14 +7767,12 @@ var ContextImpl = class ContextImpl {
|
|
|
4931
7767
|
id: crypto.randomUUID(),
|
|
4932
7768
|
baseUrl: this.origin,
|
|
4933
7769
|
keys: keyJwkPairs,
|
|
4934
|
-
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, {
|
|
4935
7771
|
actorIds: [...actorIds],
|
|
4936
|
-
sharedInbox
|
|
7772
|
+
sharedInbox,
|
|
7773
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes)
|
|
4937
7774
|
}])),
|
|
4938
|
-
activity:
|
|
4939
|
-
format: "compact",
|
|
4940
|
-
contextLoader: this.contextLoader
|
|
4941
|
-
}),
|
|
7775
|
+
activity: activityJsonLd,
|
|
4942
7776
|
activityId: activity.id?.href,
|
|
4943
7777
|
activityType: getTypeId(activity).href,
|
|
4944
7778
|
collectionSync: opts.collectionSync,
|
|
@@ -5012,8 +7846,13 @@ var ContextImpl = class ContextImpl {
|
|
|
5012
7846
|
"inbox"
|
|
5013
7847
|
]);
|
|
5014
7848
|
const contextLoader = options.contextLoader ?? this.contextLoader;
|
|
5015
|
-
|
|
5016
|
-
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
|
+
});
|
|
5017
7856
|
if (await verifyObject(Activity, json, {
|
|
5018
7857
|
contextLoader,
|
|
5019
7858
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
@@ -5046,7 +7885,7 @@ var ContextImpl = class ContextImpl {
|
|
|
5046
7885
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
5047
7886
|
});
|
|
5048
7887
|
return false;
|
|
5049
|
-
} else if (fetched.id
|
|
7888
|
+
} else if (fetched.id == null || !isSameObjectId(fetched.id, activity.id)) {
|
|
5050
7889
|
logger.debug("Fetched activity object has a different ID; failed to verify.", {
|
|
5051
7890
|
recipient,
|
|
5052
7891
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
@@ -5059,8 +7898,8 @@ var ContextImpl = class ContextImpl {
|
|
|
5059
7898
|
});
|
|
5060
7899
|
return false;
|
|
5061
7900
|
}
|
|
5062
|
-
const
|
|
5063
|
-
if (!fetched.actorIds.every((actor) => actor
|
|
7901
|
+
const activityOrigin = getAuthenticationOrigin(fetched.id);
|
|
7902
|
+
if (activityOrigin == null || !fetched.actorIds.every((actor) => getAuthenticationOrigin(actor) === activityOrigin)) {
|
|
5064
7903
|
logger.debug("Fetched activity object has actors from different origins; unable to verify.", {
|
|
5065
7904
|
recipient,
|
|
5066
7905
|
activity: await fetched.toJsonLd({ contextLoader })
|
|
@@ -5069,6 +7908,7 @@ var ContextImpl = class ContextImpl {
|
|
|
5069
7908
|
}
|
|
5070
7909
|
logger.debug("Successfully fetched the remote activity object {activityId}; ignore the original activity and use the fetched one, which is trustworthy.");
|
|
5071
7910
|
activity = fetched;
|
|
7911
|
+
json = await fetched.toJsonLd({ contextLoader });
|
|
5072
7912
|
} else logger.debug("Object Integrity Proofs are verified.", {
|
|
5073
7913
|
recipient,
|
|
5074
7914
|
activity: json
|
|
@@ -5097,13 +7937,19 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5097
7937
|
#invokedFromActorDispatcher;
|
|
5098
7938
|
#invokedFromObjectDispatcher;
|
|
5099
7939
|
request;
|
|
7940
|
+
/** Whether a diagnostic actor lookup would re-enter the actor dispatcher. */
|
|
7941
|
+
isInActorDispatcher() {
|
|
7942
|
+
return this.#invokedFromActorDispatcher != null;
|
|
7943
|
+
}
|
|
5100
7944
|
url = void 0;
|
|
7945
|
+
portableRequest;
|
|
5101
7946
|
constructor(options) {
|
|
5102
7947
|
super(options);
|
|
5103
7948
|
this.#invokedFromActorDispatcher = options.invokedFromActorDispatcher;
|
|
5104
7949
|
this.#invokedFromObjectDispatcher = options.invokedFromObjectDispatcher;
|
|
5105
7950
|
this.request = options.request;
|
|
5106
7951
|
this.url = options.url;
|
|
7952
|
+
this.portableRequest = options.portableRequest;
|
|
5107
7953
|
}
|
|
5108
7954
|
clone(data) {
|
|
5109
7955
|
return new RequestContextImpl({
|
|
@@ -5115,9 +7961,40 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5115
7961
|
invokedFromActorKeyPairsDispatcher: this.invokedFromActorKeyPairsDispatcher,
|
|
5116
7962
|
invokedFromActorDispatcher: this.#invokedFromActorDispatcher,
|
|
5117
7963
|
invokedFromObjectDispatcher: this.#invokedFromObjectDispatcher,
|
|
7964
|
+
portableRequest: this.portableRequest,
|
|
5118
7965
|
request: this.request
|
|
5119
7966
|
});
|
|
5120
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
|
+
}
|
|
5121
7998
|
async getActor(identifier, options) {
|
|
5122
7999
|
if (this.federation.actorCallbacks == null || this.federation.actorCallbacks.dispatcher == null) throw new Error("No actor dispatcher registered.");
|
|
5123
8000
|
if (this.#invokedFromActorDispatcher != null) getLogger([
|
|
@@ -5135,7 +8012,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5135
8012
|
if (actor instanceof Tombstone && options?.tombstone !== "passthrough") return null;
|
|
5136
8013
|
return actor;
|
|
5137
8014
|
}
|
|
5138
|
-
async getObject(cls, values) {
|
|
8015
|
+
async getObject(cls, values, options) {
|
|
5139
8016
|
const callbacks = this.federation.objectCallbacks[cls.typeId.href];
|
|
5140
8017
|
if (callbacks == null) throw new Error("No object dispatcher registered.");
|
|
5141
8018
|
for (const param of callbacks.parameters) if (!(param in values)) throw new TypeError(`Missing parameter: ${param}`);
|
|
@@ -5145,13 +8022,15 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5145
8022
|
actorDispatcherClass: this.#invokedFromObjectDispatcher.cls.name,
|
|
5146
8023
|
actorDispatcherValues: this.#invokedFromObjectDispatcher.values
|
|
5147
8024
|
});
|
|
5148
|
-
|
|
8025
|
+
const object = await callbacks.dispatcher(new RequestContextImpl({
|
|
5149
8026
|
...this,
|
|
5150
8027
|
invokedFromObjectDispatcher: {
|
|
5151
8028
|
cls,
|
|
5152
8029
|
values
|
|
5153
8030
|
}
|
|
5154
8031
|
}), values);
|
|
8032
|
+
if (object instanceof Tombstone && !(object instanceof cls) && options?.tombstone !== "passthrough") return null;
|
|
8033
|
+
return object;
|
|
5155
8034
|
}
|
|
5156
8035
|
#signedKey = void 0;
|
|
5157
8036
|
async getSignedKey(options = {}) {
|
|
@@ -5161,6 +8040,7 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5161
8040
|
contextLoader: options.contextLoader ?? this.contextLoader,
|
|
5162
8041
|
documentLoader: options.documentLoader ?? this.documentLoader,
|
|
5163
8042
|
timeWindow: this.federation.signatureTimeWindow,
|
|
8043
|
+
maxSignatures: this.federation.maxHttpSignatures,
|
|
5164
8044
|
meterProvider: this.meterProvider,
|
|
5165
8045
|
tracerProvider: options.tracerProvider ?? this.tracerProvider
|
|
5166
8046
|
});
|
|
@@ -5192,6 +8072,9 @@ var RequestContextImpl = class RequestContextImpl extends ContextImpl {
|
|
|
5192
8072
|
throw error;
|
|
5193
8073
|
}
|
|
5194
8074
|
}
|
|
8075
|
+
async isSignedByAudience(object, options = {}) {
|
|
8076
|
+
return await isInAudience(object, null) || await isInAudience(object, await this.getSignedKeyOwner(options), options);
|
|
8077
|
+
}
|
|
5195
8078
|
};
|
|
5196
8079
|
function forwardActivity(ctx, loggerCategory, forwarder, recipients, options) {
|
|
5197
8080
|
return ctx.tracerProvider.getTracer(name, version).startActiveSpan(ctx.federation.outboxQueue == null || options?.immediate ? `activitypub.${loggerCategory}` : "activitypub.fanout", {
|
|
@@ -5279,16 +8162,16 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5279
8162
|
if (options?.immediate) logger.debug("Forwarding activity immediately without queue since immediate option is set.");
|
|
5280
8163
|
else logger.debug("Forwarding activity immediately without queue since queue is not set.");
|
|
5281
8164
|
const promises = [];
|
|
5282
|
-
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,
|
|
5283
8167
|
keys,
|
|
5284
8168
|
activity: ctx.activity,
|
|
5285
8169
|
activityId: ctx.activityId,
|
|
5286
8170
|
activityType: ctx.activityType,
|
|
5287
|
-
inbox: new URL(inbox),
|
|
5288
8171
|
sharedInbox: inboxes[inbox].sharedInbox,
|
|
5289
8172
|
meterProvider: ctx.meterProvider,
|
|
5290
8173
|
tracerProvider: ctx.tracerProvider,
|
|
5291
|
-
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 })
|
|
5292
8175
|
}));
|
|
5293
8176
|
await Promise.all(promises);
|
|
5294
8177
|
return true;
|
|
@@ -5312,7 +8195,7 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5312
8195
|
const started = (/* @__PURE__ */ new Date()).toISOString();
|
|
5313
8196
|
const messages = [];
|
|
5314
8197
|
for (const inbox in inboxes) {
|
|
5315
|
-
const
|
|
8198
|
+
const { portableInbox, gatewayInboxes } = inboxes[inbox];
|
|
5316
8199
|
const message = {
|
|
5317
8200
|
type: "outbox",
|
|
5318
8201
|
id: crypto.randomUUID(),
|
|
@@ -5327,7 +8210,8 @@ async function forwardActivityInternal(ctx, loggerCategory, forwarder, recipient
|
|
|
5327
8210
|
started,
|
|
5328
8211
|
attempt: 0,
|
|
5329
8212
|
headers: {},
|
|
5330
|
-
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${
|
|
8213
|
+
orderingKey: orderingKey == null ? void 0 : `${orderingKey}\n${getOrderingDestination(inbox, portableInbox)}`,
|
|
8214
|
+
...getPortableMessageFields(portableInbox, gatewayInboxes),
|
|
5331
8215
|
traceContext: carrier
|
|
5332
8216
|
};
|
|
5333
8217
|
messages.push({
|
|
@@ -5459,16 +8343,18 @@ var KvSpecDeterminer = class {
|
|
|
5459
8343
|
kv;
|
|
5460
8344
|
prefix;
|
|
5461
8345
|
defaultSpec;
|
|
5462
|
-
|
|
8346
|
+
specTtl;
|
|
8347
|
+
constructor(kv, prefix, defaultSpec = "rfc9421", options = {}) {
|
|
5463
8348
|
this.kv = kv;
|
|
5464
8349
|
this.prefix = prefix;
|
|
5465
8350
|
this.defaultSpec = defaultSpec;
|
|
8351
|
+
this.specTtl = options.specTtl ?? Temporal.Duration.from({ days: 90 });
|
|
5466
8352
|
}
|
|
5467
8353
|
async determineSpec(origin) {
|
|
5468
8354
|
return await this.kv.get([...this.prefix, origin]) ?? this.defaultSpec;
|
|
5469
8355
|
}
|
|
5470
8356
|
async rememberSpec(origin, spec) {
|
|
5471
|
-
await this.kv.set([...this.prefix, origin], spec);
|
|
8357
|
+
await this.kv.set([...this.prefix, origin], spec, { ttl: this.specTtl });
|
|
5472
8358
|
}
|
|
5473
8359
|
};
|
|
5474
8360
|
function notFound(_request) {
|
|
@@ -5505,4 +8391,4 @@ function getRequestId(request) {
|
|
|
5505
8391
|
return `req_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
5506
8392
|
}
|
|
5507
8393
|
//#endregion
|
|
5508
|
-
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 };
|