@fedify/fedify 2.4.0-pr.936.41 → 2.5.0-dev.2271

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