@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.
Files changed (244) hide show
  1. package/README.md +48 -40
  2. package/dist/{assert-CJ1KXcvv.mjs → assert-D1Eafmd_.mjs} +1 -1
  3. package/dist/{assert_equals-BV7oLMxm.mjs → assert_equals-Z6IcEbWU.mjs} +2 -28
  4. package/dist/assert_exists-cffSpJ7X.mjs +28 -0
  5. package/dist/assert_false-CPdRnc3E.mjs +23 -0
  6. package/dist/assert_greater_or_equal-BlafOF4-.mjs +31 -0
  7. package/dist/{assert_instance_of-Q9KTBnrf.mjs → assert_instance_of-mrXWWNY-.mjs} +1 -1
  8. package/dist/{assert_not_equals-EPJPoxuf.mjs → assert_not_equals-CpPK2t89.mjs} +2 -1
  9. package/dist/assert_rejects-CTptdYck.mjs +39 -0
  10. package/dist/{assert_strict_equals-Ec20AUwa.mjs → assert_strict_equals-CYvBmkpC.mjs} +2 -1
  11. package/dist/{assert_throws-NiFfqbnH.mjs → assert_throws-BYITOvPO.mjs} +2 -1
  12. package/dist/assertion_error-B-cx0fFA.mjs +31 -0
  13. package/dist/audience-CHQ-FlLD.mjs +95 -0
  14. package/dist/{builder-BBPwtB2v.mjs → builder-lJDh222H.mjs} +169 -14
  15. package/dist/{circuit-breaker-CBGyz5F7.mjs → circuit-breaker-XJ53md4S.mjs} +224 -17
  16. package/dist/circuit-breaker-test-utils-DW6YC_Pe.mjs +14 -0
  17. package/dist/{client-CjF5IYQS.mjs → client-CEnNz8O7.mjs} +144 -8
  18. package/dist/{codec-BBga-FWI.mjs → codec-C6rTrpIp.mjs} +26 -3
  19. package/dist/{collection-BIhlldqK.mjs → collection-CnnP1UAv.mjs} +1 -1
  20. package/dist/compat/mod.cjs +1 -1
  21. package/dist/compat/mod.d.cts +1 -1
  22. package/dist/compat/mod.d.ts +1 -1
  23. package/dist/compat/mod.js +1 -1
  24. package/dist/compat/outgoing-jsonld.test.mjs +71 -3
  25. package/dist/compat/public-audience.test.mjs +14 -5
  26. package/dist/compat/secured-document.test.d.mts +2 -0
  27. package/dist/compat/secured-document.test.mjs +58 -0
  28. package/dist/compat/transformers.test.mjs +5 -5
  29. package/dist/compound-proof-zAjqsHiN.mjs +989 -0
  30. package/dist/{context-Cjnr0ig1.d.ts → context-7NyiqIcg.d.ts} +1600 -25
  31. package/dist/{context-BJXxPElm.mjs → context-8Tu9kea0.mjs} +21 -63
  32. package/dist/{context-C05Rgxmu.d.cts → context-tsYPJ2Ew.d.cts} +1600 -25
  33. package/dist/{deno-D3c0o_N0.mjs → deno-D8wr4ikF.mjs} +1 -1
  34. package/dist/dist-DJQM4u7i.mjs +189 -0
  35. package/dist/docloader-TCvsU9vF.mjs +87 -0
  36. package/dist/{esm-yYQfPhIY.mjs → esm-BHXXx4Wx.mjs} +13 -6
  37. package/dist/federation/audience.test.d.mts +2 -0
  38. package/dist/federation/audience.test.mjs +130 -0
  39. package/dist/federation/builder.test.mjs +54 -5
  40. package/dist/federation/circuit-breaker.test.mjs +838 -7
  41. package/dist/federation/collection.test.mjs +3 -3
  42. package/dist/federation/compound-fanout.test.d.mts +2 -0
  43. package/dist/federation/compound-fanout.test.mjs +228 -0
  44. package/dist/federation/handler-compound-proof.test.d.mts +2 -0
  45. package/dist/federation/handler-compound-proof.test.mjs +384 -0
  46. package/dist/federation/handler.test.mjs +387 -15
  47. package/dist/federation/hashlink-media.test.d.mts +2 -0
  48. package/dist/federation/hashlink-media.test.mjs +422 -0
  49. package/dist/federation/idempotency.test.mjs +6 -6
  50. package/dist/federation/inbox-report.test.d.mts +2 -0
  51. package/dist/federation/inbox-report.test.mjs +475 -0
  52. package/dist/federation/inbox.test.mjs +3 -3
  53. package/dist/federation/keycache.test.mjs +215 -14
  54. package/dist/federation/kv.test.mjs +3 -3
  55. package/dist/federation/metrics.test.mjs +5 -5
  56. package/dist/federation/middleware.test.mjs +967 -35
  57. package/dist/federation/mod.cjs +18 -540
  58. package/dist/federation/mod.d.cts +5 -5
  59. package/dist/federation/mod.d.ts +5 -5
  60. package/dist/federation/mod.js +1 -1
  61. package/dist/federation/mq.test.mjs +104 -47
  62. package/dist/federation/negotiation.test.mjs +4 -4
  63. package/dist/federation/outgoing-proof.test.d.mts +2 -0
  64. package/dist/federation/outgoing-proof.test.mjs +931 -0
  65. package/dist/federation/portable-actor.test.d.mts +2 -0
  66. package/dist/federation/portable-actor.test.mjs +496 -0
  67. package/dist/federation/portable-chain.test.d.mts +2 -0
  68. package/dist/federation/portable-chain.test.mjs +370 -0
  69. package/dist/federation/portable-collection.test.d.mts +2 -0
  70. package/dist/federation/portable-collection.test.mjs +832 -0
  71. package/dist/federation/portable-delivery.test.d.mts +2 -0
  72. package/dist/federation/portable-delivery.test.mjs +746 -0
  73. package/dist/federation/portable-gateway-keys.test.d.mts +2 -0
  74. package/dist/federation/portable-gateway-keys.test.mjs +686 -0
  75. package/dist/federation/portable-inbox.test.d.mts +2 -0
  76. package/dist/federation/portable-inbox.test.mjs +1535 -0
  77. package/dist/federation/portable-parse-uri.test.d.mts +2 -0
  78. package/dist/federation/portable-parse-uri.test.mjs +348 -0
  79. package/dist/federation/portable-route-activity.test.d.mts +2 -0
  80. package/dist/federation/portable-route-activity.test.mjs +259 -0
  81. package/dist/federation/portable.test.d.mts +2 -0
  82. package/dist/federation/portable.test.mjs +1072 -0
  83. package/dist/federation/retry.test.mjs +4 -3
  84. package/dist/federation/router.test.mjs +9 -159
  85. package/dist/federation/send.test.mjs +245 -27
  86. package/dist/federation/tasks/codec.test.mjs +4 -4
  87. package/dist/federation/tasks/enqueue.test.mjs +5 -4
  88. package/dist/federation/tasks/tasks.test.mjs +7 -6
  89. package/dist/federation/temporal.test.mjs +4 -4
  90. package/dist/federation/webfinger.test.mjs +275 -12
  91. package/dist/{middleware-BW7WQltk.cjs → federation-Dfl-i7Jt.cjs} +6098 -895
  92. package/dist/{http-Dm0JAUa_.mjs → http-BAgKAAGY.mjs} +397 -168
  93. package/dist/http-CYO4wjB5.cjs +10630 -0
  94. package/dist/http-RW_i_Nkq.js +6527 -0
  95. package/dist/key-C1nv7_jD.d.ts +1125 -0
  96. package/dist/key-D979vduq.mjs +951 -0
  97. package/dist/key-DYxuyd-s.d.cts +1125 -0
  98. package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
  99. package/dist/keycache-Da8A8mZa.mjs +200 -0
  100. package/dist/{kv-D6hNiMTK.d.ts → kv-B2mzK5tl.d.cts} +3 -2
  101. package/dist/{kv-gJ8LYbxX.d.cts → kv-B2mzK5tl.d.ts} +3 -2
  102. package/dist/kv-DBRWVx3Q.mjs +104 -0
  103. package/dist/{kv-cache-BxUZRBC2.js → kv-cache-Cqfdytdd.js} +54 -16
  104. package/dist/{kv-cache-B7H7lZ84.mjs → kv-cache-Gh05Xv-P.mjs} +1 -1
  105. package/dist/{kv-cache-D-hbjJAD.cjs → kv-cache-zd0dNGxi.cjs} +55 -19
  106. package/dist/{ld-DAPAVL_7.mjs → ld-wmqC-95l.mjs} +44 -12
  107. package/dist/map-local-context-conflict-DOYEEJ3u.mjs +204 -0
  108. package/dist/map-local-create-note-B0SeXJms.mjs +214 -0
  109. package/dist/{metrics-BMaatuZi.mjs → metrics-DPdi_XK7.mjs} +3 -2
  110. package/dist/metrics-DVKYY6K1.d.cts +36 -0
  111. package/dist/metrics-DoRtI0qU.d.ts +36 -0
  112. package/dist/{middleware-O2LHXFEk.mjs → middleware-CqQ5Qahj.mjs} +3155 -269
  113. package/dist/{middleware-CfZR_riY.js → middleware-DbCI_eni.js} +5527 -865
  114. package/dist/{middleware-DfPBAh9L.mjs → middleware-DjEMIAP5.mjs} +1 -1
  115. package/dist/{mod-DdONo-ep.d.cts → mod-BojP2Bw3.d.cts} +1 -1
  116. package/dist/mod-CavhAgLw.d.cts +320 -0
  117. package/dist/mod-CtpF2BU5.d.ts +320 -0
  118. package/dist/{mod-CNef4in0.d.ts → mod-DEZXwV6J.d.ts} +9 -3
  119. package/dist/{mod-BrpVsSfY.d.cts → mod-Dw-nPxQh.d.cts} +9 -3
  120. package/dist/{mod-BsCE5Ihq.d.ts → mod-Xj5P4s4d.d.ts} +1 -1
  121. package/dist/mod.cjs +40 -39
  122. package/dist/mod.d.cts +10 -11
  123. package/dist/mod.d.ts +10 -11
  124. package/dist/mod.js +8 -8
  125. package/dist/nodeinfo/client.test.mjs +139 -4
  126. package/dist/nodeinfo/handler.test.mjs +5 -5
  127. package/dist/nodeinfo/mod.cjs +1 -1
  128. package/dist/nodeinfo/mod.d.cts +1 -1
  129. package/dist/nodeinfo/mod.d.ts +1 -1
  130. package/dist/nodeinfo/mod.js +1 -1
  131. package/dist/nodeinfo/types.test.mjs +4 -4
  132. package/dist/otel/exporter.test.mjs +3 -3
  133. package/dist/otel/mod.cjs +0 -1
  134. package/dist/otel/mod.d.cts +1 -1
  135. package/dist/otel/mod.d.ts +1 -1
  136. package/dist/{outgoing-jsonld-URuqsICz.mjs → outgoing-jsonld-4IJqPYos.mjs} +12 -10
  137. package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
  138. package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
  139. package/dist/owner-BfygwLNy.mjs +267 -0
  140. package/dist/portable-collection-DSrR0BSV.js +670 -0
  141. package/dist/portable-collection-L8jCHlci.cjs +686 -0
  142. package/dist/portable-collection-idA006Sj.mjs +418 -0
  143. package/dist/portable-key-CA6u-dST.mjs +549 -0
  144. package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
  145. package/dist/proof-DeC9pb6t.mjs +1299 -0
  146. package/dist/{public-audience-Cr5MsnMq.mjs → public-audience--klBIOk1.mjs} +15 -10
  147. package/dist/router-u5O5KCQr.mjs +156 -0
  148. package/dist/runtime/mod.cjs +1 -1
  149. package/dist/runtime/mod.js +1 -1
  150. package/dist/secured-document-CxzZZ9nL.mjs +35 -0
  151. package/dist/send-1tcGhW1h.mjs +495 -0
  152. package/dist/sig/accept.test.mjs +1 -1
  153. package/dist/sig/compound-proof-discovery.test.d.mts +2 -0
  154. package/dist/sig/compound-proof-discovery.test.mjs +331 -0
  155. package/dist/sig/compound-proof-producer.test.d.mts +2 -0
  156. package/dist/sig/compound-proof-producer.test.mjs +107 -0
  157. package/dist/sig/compound-proof-verification.test.d.mts +2 -0
  158. package/dist/sig/compound-proof-verification.test.mjs +833 -0
  159. package/dist/sig/compound-proof.test.d.mts +2 -0
  160. package/dist/sig/compound-proof.test.mjs +411 -0
  161. package/dist/sig/http.test.mjs +702 -31
  162. package/dist/sig/key-owner-evidence.test.d.mts +2 -0
  163. package/dist/sig/key-owner-evidence.test.mjs +100 -0
  164. package/dist/sig/key.test.mjs +220 -8
  165. package/dist/sig/ld.test.mjs +65 -7
  166. package/dist/sig/mitra-receive-interop.test.d.mts +2 -0
  167. package/dist/sig/mitra-receive-interop.test.mjs +260 -0
  168. package/dist/sig/mitra-send-interop.test.d.mts +2 -0
  169. package/dist/sig/mitra-send-interop.test.mjs +461 -0
  170. package/dist/sig/mod.cjs +18 -16
  171. package/dist/sig/mod.d.cts +4 -4
  172. package/dist/sig/mod.d.ts +4 -4
  173. package/dist/sig/mod.js +3 -3
  174. package/dist/sig/owner.test.mjs +108 -9
  175. package/dist/sig/portable-collection.test.d.mts +2 -0
  176. package/dist/sig/portable-collection.test.mjs +697 -0
  177. package/dist/sig/portable-interop.test.d.mts +2 -0
  178. package/dist/sig/portable-interop.test.mjs +156 -0
  179. package/dist/sig/portable-key-id.test.d.mts +2 -0
  180. package/dist/sig/portable-key-id.test.mjs +67 -0
  181. package/dist/sig/portable-key.test.d.mts +2 -0
  182. package/dist/sig/portable-key.test.mjs +1439 -0
  183. package/dist/sig/proof.test.mjs +1753 -16
  184. package/dist/sig/tootik-request-interop.test.d.mts +2 -0
  185. package/dist/sig/tootik-request-interop.test.mjs +190 -0
  186. package/dist/sig/tootik-send-interop.test.d.mts +2 -0
  187. package/dist/sig/tootik-send-interop.test.mjs +331 -0
  188. package/dist/sig/verification.test.d.mts +2 -0
  189. package/dist/sig/verification.test.mjs +310 -0
  190. package/dist/std__assert-DvMDRPes.mjs +8 -0
  191. package/dist/{testing-CO7ftO8A.mjs → tasks-IG3pHfvZ.mjs} +2 -2
  192. package/dist/{temporal-yE5EcfNX.mjs → temporal-DA7r1fm3.mjs} +3 -3
  193. package/dist/testing/context.test.d.mts +2 -0
  194. package/dist/testing/context.test.mjs +19 -0
  195. package/dist/testing/kv.test.d.mts +2 -0
  196. package/dist/testing/kv.test.mjs +101 -0
  197. package/dist/testing/mod.d.mts +1604 -48
  198. package/dist/testing/mod.mjs +6 -3
  199. package/dist/testing-t8xDeJZ5.mjs +8 -0
  200. package/dist/{transformers-DGwl-hD1.cjs → transformers-BalCrSsr.cjs} +0 -1
  201. package/dist/{types-DC1pNgJA.js → types-_S2_MeLF.js} +144 -8
  202. package/dist/{types-CPym83zc.cjs → types-poIao40f.cjs} +160 -7
  203. package/dist/utils/docloader.test.mjs +530 -8
  204. package/dist/utils/kv-cache.test.mjs +2 -2
  205. package/dist/utils/mod.cjs +1 -1
  206. package/dist/utils/mod.d.cts +1 -1
  207. package/dist/utils/mod.d.ts +1 -1
  208. package/dist/utils/mod.js +1 -1
  209. package/dist/verification-C3954hQh.mjs +5 -0
  210. package/dist/verification-Dpfcw8ml.mjs +105 -0
  211. package/dist/vocab/cjs.test.mjs +1 -1
  212. package/package.json +9 -9
  213. package/dist/assert_rejects-EgstcQy5.mjs +0 -82
  214. package/dist/docloader-BCecHIxU.mjs +0 -49
  215. package/dist/http-Ctb_KhZ8.js +0 -2483
  216. package/dist/http-VyDTd4G3.d.cts +0 -553
  217. package/dist/http-jr0TkgZe.cjs +0 -2694
  218. package/dist/http-lf8Hsd91.d.ts +0 -553
  219. package/dist/key-C_gNKtxm.mjs +0 -453
  220. package/dist/keycache-_DHqz5z_.mjs +0 -97
  221. package/dist/metrics-DN13mCAw.d.cts +0 -294
  222. package/dist/metrics-v_e0n5GQ.d.ts +0 -294
  223. package/dist/mod-B0hW12_O.d.cts +0 -307
  224. package/dist/mod-COIAjwRS.d.ts +0 -307
  225. package/dist/owner-Dcx-czqn.mjs +0 -120
  226. package/dist/proof-BkAxvrjt.mjs +0 -316
  227. package/dist/proof-D-H_PnJW.js +0 -1415
  228. package/dist/proof-NY0D8Oux.cjs +0 -1562
  229. package/dist/send-D_j7E-oC.mjs +0 -239
  230. package/dist/std__assert-B2NnY9bq.mjs +0 -79
  231. package/dist/temporal-polyfill-BCdTHexd.cjs +0 -3565
  232. /package/dist/{accept-8PiolDP7.mjs → accept-CPVqki78.mjs} +0 -0
  233. /package/dist/{activity-listener-BtTSxZWA.mjs → activity-listener-C-BSbKI7.mjs} +0 -0
  234. /package/dist/{client-CAM_bQXx.d.cts → client-D-XP_Ccb.d.cts} +0 -0
  235. /package/dist/{client-CSddvgWN.d.ts → client-D-XP_Ccb.d.ts} +0 -0
  236. /package/dist/{keys-Dbyd7bhl.mjs → keys-DIJNHW-Y.mjs} +0 -0
  237. /package/dist/{kv-CXBAok3k.mjs → kv-DCHkaJ9-.mjs} +0 -0
  238. /package/dist/{mq-Bo7Etgh6.mjs → mq-Dm1Wmtm-.mjs} +0 -0
  239. /package/dist/{negotiation-RuwrPFqi.mjs → negotiation-CCocWT_N.mjs} +0 -0
  240. /package/dist/{retry-DgFqeS5k.mjs → retry-zX5xw5e0.mjs} +0 -0
  241. /package/dist/{chunk-D5IdezsZ.js → rolldown-runtime-BC-Eo5Mj.js} +0 -0
  242. /package/dist/{chunk-Czpeue4M.cjs → rolldown-runtime-Dmm3t92W.cjs} +0 -0
  243. /package/dist/{transformers-DXze6FUB.js → transformers-DAtXl_Kc.js} +0 -0
  244. /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 version, t as name } from "./deno-D3c0o_N0.mjs";
5
- import { a as instrumentDocumentLoader, b as recordWebFingerHandle, c as recordCircuitBreakerStateChange, d as recordCollectionRequest, f as recordCollectionTotalItems, g as recordInboxActivity, h as recordFanoutRecipients, i as getRemoteHost, l as recordCollectionDispatchDuration, n as getDurationMs, o as isAbortError, r as getFederationMetrics, u as recordCollectionPageItems, v as recordOutboxActivity, x as registerQueueDepthGauge, y as recordOutboxEnqueue } from "./metrics-BMaatuZi.mjs";
6
- import { t as formatAcceptSignature } from "./accept-8PiolDP7.mjs";
7
- import { a as importJwk, o as validateCryptoKey, t as exportJwk } from "./key-C_gNKtxm.mjs";
8
- import { l as verifyRequest, o as parseRfc9421SignatureInput, u as verifyRequestDetailed } from "./http-Dm0JAUa_.mjs";
9
- import { t as getAuthenticatedDocumentLoader } from "./docloader-BCecHIxU.mjs";
10
- import { n as kvCache } from "./kv-cache-B7H7lZ84.mjs";
11
- 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-DAPAVL_7.mjs";
12
- import { n as getKeyOwner, t as doesActorOwnKey } from "./owner-Dcx-czqn.mjs";
13
- import { r as normalizeOutgoingActivityJsonLd } from "./outgoing-jsonld-URuqsICz.mjs";
14
- import { i as verifyObject, n as hasProofLike, r as signObject } from "./proof-BkAxvrjt.mjs";
15
- import { t as getNodeInfo } from "./client-CjF5IYQS.mjs";
16
- import { t as nodeInfoToJson } from "./types-CrtdVuDP.mjs";
17
- import { n as extractInboxes, r as sendActivity, t as SendActivityError } from "./send-D_j7E-oC.mjs";
18
- import { n as FederationBuilderImpl, t as ACTOR_ALIAS_PREFIX } from "./builder-BBPwtB2v.mjs";
19
- import { t as CircuitBreaker } from "./circuit-breaker-CBGyz5F7.mjs";
20
- import { t as buildCollectionSynchronizationHeader } from "./collection-BIhlldqK.mjs";
21
- import { t as KvKeyCache } from "./keycache-_DHqz5z_.mjs";
22
- import { t as acceptsJsonLd } from "./negotiation-RuwrPFqi.mjs";
23
- import { t as hasMalformedKnownTemporalLiteral } from "./temporal-yE5EcfNX.mjs";
24
- import { t as createExponentialBackoffPolicy } from "./retry-DgFqeS5k.mjs";
25
- import { t as TaskCodec } from "./codec-BBga-FWI.mjs";
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
- const BUILT_IN_COLLECTION_METRIC_KINDS = new Set([
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$1, identifier, uriGetter, filter, filterPredicate, context, collectionCallbacks, tracerProvider, meterProvider, onUnauthorized, onNotFound }) {
641
- const spanName = name$1.trim().replace(/\s+/g, "_");
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$1),
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$1, filterPredicate);
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
- totalItems: totalItemCount ?? null,
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(context.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(context.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$1}`, {
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$1, filterPredicate);
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(context.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(context.url);
1866
+ next = new URL(pageBase);
771
1867
  next.searchParams.set("cursor", nextCursor);
772
1868
  }
773
- const partOf = new URL(context.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.href === expectedActorId.href)) {
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().json();
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
- const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx);
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) return 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 (routeResult === "alreadyProcessed") return new Response(`Activity <${activity.id}> has already been processed.`, {
1652
- status: 202,
1653
- headers: { "Content-Type": "text/plain; charset=utf-8" }
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 if (routeResult === "error") return new Response("Internal server error.", {
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 return new Response("", {
1672
- status: 202,
1673
- headers: { "Content-Type": "text/plain; charset=utf-8" }
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
- const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, Collection, CollectionPage, filterPredicate).fetchCollection(cursor);
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
- const handler = new CustomCollectionHandler(name, values, context, callbacks, tracerProvider, meterProvider, OrderedCollection, OrderedCollectionPage, filterPredicate).fetchCollection(cursor);
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 response = await handler.toJsonLd().then(respondAsActivity);
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$2, values, context, callbacks, tracerProvider = trace.getTracerProvider(), meterProvider, Collection, CollectionPage, filterPredicate) {
1799
- this.name = name$2;
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 = new URL(resource);
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
- identifier = await mapUsernameToIdentifier(match[1]);
2601
- resourceUrl = new URL(`acct:${match[1]}@${normalizedHost}`);
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: context.getActorUri(identifier).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
- const preferredUsername = actor.preferredUsername;
2643
- if (resourceUrl.protocol != "acct:" && preferredUsername != null) {
2644
- aliases.push(`acct:${preferredUsername}@${host ?? context.url.host}`);
2645
- if (host != null && host !== context.url.host) aliases.push(`acct:${preferredUsername}@${context.url.host}`);
2646
- }
2647
- if (resourceUrl.href !== context.getActorUri(identifier).href) aliases.push(context.getActorUri(identifier).href);
2648
- if (resourceUrl.protocol === "acct:" && host != null && host !== context.url.host && !resourceUrl.href.endsWith(`@${host}`)) {
2649
- const username = resourceUrl.href.replace(/^acct:/, "").replace(/@.*$/, "");
2650
- aliases.push(`acct:${username}@${host}`);
2651
- }
2652
- const jrd = {
2653
- subject: resourceUrl.href,
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
- specDeterminer: new KvSpecDeterminer(this.kv, this.kvPrefixes.httpMessageSignaturesSpec, options.firstKnock),
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 = () => Activity.fromJsonLd(message.activity, {
3250
- contextLoader: this.contextLoaderFactory(loaderOptions),
3251
- documentLoader: rsaKeyPair == null ? this.documentLoaderFactory(loaderOptions) : this.authenticatedDocumentLoaderFactory(rsaKeyPair, loaderOptions),
3252
- tracerProvider: this.tracerProvider
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
- for await (const _ of activity.getProofs({ contextLoader })) {
3900
- hasProof = true;
3901
- break;
3902
- }
3903
- if (!hasProof) {
3904
- for (const { keyId, privateKey } of keys) if (privateKey.algorithm.name === "Ed25519") {
3905
- activity = await signObject(activity, privateKey, keyId, {
3906
- contextLoader,
3907
- tracerProvider: this.tracerProvider
3908
- });
3909
- hasProof = true;
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
- 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.", {
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(sendActivity({
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 inboxOrigin = new URL(inbox).origin;
3987
- const messageOrderingKey = orderingKey == null ? void 0 : `${orderingKey}\n${inboxOrigin}`;
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 (acceptsJsonLd(request)) response.headers.set("Vary", "Accept");
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
- context = this.#createContext(request, contextData, { documentLoader: await context.getDocumentLoader({ identifier: route.values.identifier }) });
4261
- case "sharedInbox": {
4262
- if (routeName !== "inbox" && this.sharedInboxKeyDispatcher != null) {
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
- if (!this.manuallyStartQueue) this._startQueueInternal(contextData);
4267
- const inboxContextFactory = context.toInboxContext.bind(context);
4268
- inboxContextFactory[rawInboxContextFactorySymbol] = context.toInboxContext.bind(context);
4269
- return await handleInbox(request, {
4270
- recipient: route.values.identifier ?? null,
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
- let baseUrl = url.searchParams.get("base-url");
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 ? ((i) => (i instanceof URL ? i.href : i.id?.href ?? "").startsWith(baseUrl)) : void 0,
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.replace(/^collection:/, "");
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.replace(/^orderedCollection:/, "");
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 new URL(path, this.canonicalOrigin);
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 new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("outbox", { identifier });
4537
- if (path == null) throw new RouterError("No outbox dispatcher registered.");
4538
- return new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("following", { identifier });
4557
- if (path == null) throw new RouterError("No following collection path registered.");
4558
- return new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("followers", { identifier });
4562
- if (path == null) throw new RouterError("No followers collection path registered.");
4563
- return new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("liked", { identifier });
4567
- if (path == null) throw new RouterError("No liked collection path registered.");
4568
- return new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("featured", { identifier });
4572
- if (path == null) throw new RouterError("No featured collection path registered.");
4573
- return new URL(path, this.canonicalOrigin);
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
- const path = this.federation.router.build("featuredTags", { identifier });
4577
- if (path == null) throw new RouterError("No featured tags collection path registered.");
4578
- return new URL(path, this.canonicalOrigin);
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
- parseUri(uri) {
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 !== this.origin && uri.origin !== this.canonicalOrigin) return null;
4588
- const route = this.federation.router.route(uri.pathname);
4589
- if (route == null) return null;
4590
- else if (route.name === "sharedInbox") return {
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, name] = match;
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
- async getKeyPairsFromIdentifier(identifier) {
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}`, actorUri)
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
- let proofCreated = false;
4888
- if (actorKeyPairs != null) {
4889
- const contextLoader = this.contextLoader;
4890
- for (const kp of actorKeyPairs) {
4891
- if (kp.privateKey.algorithm.name !== "Ed25519" || kp.multikey.id == null) continue;
4892
- activity = await signObject(activity, kp.privateKey, kp.multikey.id, {
4893
- contextLoader,
4894
- tracerProvider: this.tracerProvider
4895
- });
4896
- proofCreated = true;
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: await activity.toJsonLd({
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
- const json = await activity.toJsonLd({ contextLoader });
5016
- const keyCache = new KvKeyCache(this.federation.kv, this.federation.kvPrefixes.publicKey, this);
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?.href !== activity.id.href) {
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 activityId = fetched.id;
5063
- if (!fetched.actorIds.every((actor) => actor.origin === activityId.origin)) {
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
- return await callbacks.dispatcher(new RequestContextImpl({
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(sendActivity({
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 inboxUrl = new URL(inbox);
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${inboxUrl.origin}`,
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
- constructor(kv, prefix, defaultSpec = "rfc9421") {
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, autoIdAssigner as b, handleActor as c, handleInbox as d, handleObject as f, handleBenchmarkTrigger as g, respondWithObjectIfAcceptable as h, KvSpecDeterminer as i, handleCollection as l, respondWithObject as m, FederationImpl as n, createFederation as o, handleOutbox as p, InboxContextImpl as r, handleWebFinger as s, ContextImpl as t, handleCustomCollection as u, handleNodeInfoJrd as v, actorDehydrator as y };
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 };