@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,43 +1,48 @@
1
1
  import { Temporal } from "temporal-polyfill";
2
2
  import "urlpattern-polyfill";
3
3
  globalThis.addEventListener = () => {};
4
- import { t as esm_default } from "../esm-yYQfPhIY.mjs";
5
- import { t as MemoryKvStore } from "../kv-CXBAok3k.mjs";
6
- import { a as numberSchema, r as envelopeSchema, t as MockQueue } from "../testing-CO7ftO8A.mjs";
7
- import { t as assertEquals } from "../assert_equals-BV7oLMxm.mjs";
8
- import { r as assertExists } from "../std__assert-B2NnY9bq.mjs";
9
- import { r as assertFalse, t as assertRejects } from "../assert_rejects-EgstcQy5.mjs";
10
- import { t as assertInstanceOf } from "../assert_instance_of-Q9KTBnrf.mjs";
11
- import { t as assertThrows } from "../assert_throws-NiFfqbnH.mjs";
12
- import { t as assertNotEquals } from "../assert_not_equals-EPJPoxuf.mjs";
13
- import { t as assertStrictEquals } from "../assert_strict_equals-Ec20AUwa.mjs";
14
- import { t as assert } from "../assert-CJ1KXcvv.mjs";
15
- import { g as recordInboxActivity } from "../metrics-DMk-nTxI.mjs";
16
- import { l as verifyRequest, s as signRequest } from "../http-CPVMUgsW.mjs";
17
- import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-Dbyd7bhl.mjs";
18
- import { t as getAuthenticatedDocumentLoader } from "../docloader-e1khNUKy.mjs";
19
- import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-Cj9F_5AP.mjs";
20
- import { t as doesActorOwnKey } from "../owner-Br9YYzn_.mjs";
21
- import { i as verifyObject, r as signObject } from "../proof-B4ngoaNQ.mjs";
22
- import { g as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-CvwTUgnP.mjs";
23
- import { t as CircuitBreaker } from "../circuit-breaker-CBGyz5F7.mjs";
24
- import { t as TaskCodec } from "../codec-BBga-FWI.mjs";
4
+ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { t as ManualClockKvStore } from "../kv-DBRWVx3Q.mjs";
7
+ import { a as numberSchema, r as envelopeSchema, t as MockQueue } from "../tasks-IG3pHfvZ.mjs";
8
+ import "../testing-t8xDeJZ5.mjs";
9
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
10
+ import { t as assertExists } from "../assert_exists-cffSpJ7X.mjs";
11
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
12
+ import { t as assertInstanceOf } from "../assert_instance_of-mrXWWNY-.mjs";
13
+ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
14
+ import { t as assertNotEquals } from "../assert_not_equals-CpPK2t89.mjs";
15
+ import "../std__assert-DvMDRPes.mjs";
16
+ import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
17
+ import { t as assertStrictEquals } from "../assert_strict_equals-CYvBmkpC.mjs";
18
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
19
+ import { g as recordInboxActivity } from "../metrics-CYg9yxBL.mjs";
20
+ import { a as compactJsonLd, h as verifyJsonLd, p as signJsonLd, s as detachSignature } from "../ld-CzaP-hJl.mjs";
21
+ import { a as signObject, s as verifyObject } from "../proof-Dxy6ByqI.mjs";
22
+ import { f as verifyRequest, l as signRequest } from "../http-siciWkmx.mjs";
23
+ import { a as rsaPrivateKey3, c as rsaPublicKey3, i as rsaPrivateKey2, n as ed25519PrivateKey, r as ed25519PublicKey, s as rsaPublicKey2, t as ed25519Multikey } from "../keys-DIJNHW-Y.mjs";
24
+ import { t as getAuthenticatedDocumentLoader } from "../docloader-DBoYbvYW.mjs";
25
+ import { T as handleBenchmarkTrigger, i as KvSpecDeterminer, n as FederationImpl, o as createFederation, r as InboxContextImpl, t as ContextImpl } from "../middleware-D7_LO-Vs.mjs";
26
+ import { t as doesActorOwnKey } from "../owner-DYgfTFHy.mjs";
27
+ import { t as CircuitBreaker } from "../circuit-breaker-XJ53md4S.mjs";
28
+ import { t as TaskCodec } from "../codec-C6rTrpIp.mjs";
29
+ import { t as markCircuitBreakerLegacySweepDone } from "../circuit-breaker-test-utils-DW6YC_Pe.mjs";
25
30
  import { configure, reset } from "@logtape/logtape";
26
31
  import { RouterError } from "@fedify/uri-template";
27
32
  import * as vocab from "@fedify/vocab";
28
33
  import { Create, Note, Offer, Person, getTypeId, lookupObject } from "@fedify/vocab";
29
34
  import { SpanStatusCode, metrics } from "@opentelemetry/api";
35
+ import { FetchError, UrlError, getDocumentLoader } from "@fedify/vocab-runtime";
30
36
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
31
- import { FetchError, getDocumentLoader } from "@fedify/vocab-runtime";
32
- import { deepStrictEqual, ok, rejects, strictEqual } from "node:assert/strict";
37
+ import dns from "node:dns/promises";
38
+ import { deepStrictEqual, ok, rejects, strictEqual, throws } from "node:assert/strict";
33
39
  import serialize from "json-canon";
34
40
  import { DataPointType, MeterProvider as MeterProvider$1, MetricReader } from "@opentelemetry/sdk-metrics";
35
41
  //#region ../fixture/src/fixtures/example.com/create.json
36
- var id$2 = "https://example.com/create";
37
42
  var create_default = {
38
43
  "@context": "https://www.w3.org/ns/activitystreams",
39
44
  type: "Create",
40
- id: id$2,
45
+ id: "https://example.com/create",
41
46
  actor: "https://example.com/person"
42
47
  };
43
48
  //#endregion
@@ -1171,6 +1176,90 @@ test({
1171
1176
  esm_default.hardReset();
1172
1177
  }
1173
1178
  });
1179
+ test("Federation.fetch() [rfc9421] multiple POST signatures", async (t) => {
1180
+ for (const [name, validSecond, maxHttpSignatures] of [
1181
+ [
1182
+ "valid second signature",
1183
+ true,
1184
+ void 0
1185
+ ],
1186
+ [
1187
+ "both signatures invalid",
1188
+ false,
1189
+ void 0
1190
+ ],
1191
+ [
1192
+ "valid second signature beyond maxHttpSignatures",
1193
+ true,
1194
+ 1
1195
+ ]
1196
+ ]) await t.step(name, async () => {
1197
+ const inbox = [];
1198
+ const federation = createFederation({
1199
+ kv: new MemoryKvStore(),
1200
+ documentLoaderFactory: () => mockDocumentLoader,
1201
+ contextLoaderFactory: () => mockDocumentLoader,
1202
+ maxHttpSignatures
1203
+ });
1204
+ federation.setActorDispatcher("/users/{identifier}", (ctx, identifier) => new vocab.Person({ id: ctx.getActorUri(identifier) }));
1205
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, activity) => {
1206
+ inbox.push(activity.id.href);
1207
+ });
1208
+ const signed = await signRequest(new Request("https://example.com/inbox", {
1209
+ method: "POST",
1210
+ headers: { "Content-Type": "application/activity+json" },
1211
+ body: JSON.stringify(create_default)
1212
+ }), rsaPrivateKey2, rsaPublicKey2.id, { spec: "rfc9421" });
1213
+ const input = signed.headers.get("Signature-Input");
1214
+ const signature = signed.headers.get("Signature");
1215
+ signed.headers.set("Signature-Input", `${input}, ${input.replace(/^sig1=/, "sig2=")}`);
1216
+ signed.headers.set("Signature", `sig1=:AAAAAA==:, ${validSecond ? signature.replace(/^sig1=/, "sig2=") : "sig2=:AAAAAA==:"}`);
1217
+ const response = await federation.fetch(signed, { contextData: void 0 });
1218
+ const accepted = validSecond && maxHttpSignatures == null;
1219
+ assertEquals(response.status, accepted ? 202 : 401);
1220
+ assertEquals(inbox, accepted ? [create_default.id] : []);
1221
+ });
1222
+ });
1223
+ test("FederationOptions.maxHttpSignatures", async (t) => {
1224
+ const kv = new MemoryKvStore();
1225
+ await t.step("defaults to three", () => {
1226
+ const federation = createFederation({ kv });
1227
+ assertInstanceOf(federation, FederationImpl);
1228
+ assertEquals(federation.maxHttpSignatures, 3);
1229
+ });
1230
+ await t.step("rejects invalid values", () => {
1231
+ for (const maxHttpSignatures of [
1232
+ 0,
1233
+ -1,
1234
+ 1.5,
1235
+ NaN,
1236
+ 2 ** 53,
1237
+ null
1238
+ ]) assertThrows(() => createFederation({
1239
+ kv,
1240
+ maxHttpSignatures
1241
+ }), RangeError);
1242
+ const federation = createFederation({
1243
+ kv,
1244
+ maxHttpSignatures: Infinity
1245
+ });
1246
+ assertInstanceOf(federation, FederationImpl);
1247
+ assertEquals(federation.maxHttpSignatures, Infinity);
1248
+ });
1249
+ await t.step("applies to RequestContext.getSignedKey()", async () => {
1250
+ let request = new Request("https://example.com/", { headers: { "Accept": "application/ld+json" } });
1251
+ for (const [label, keyId] of [["sig1", new URL("https://example.com/missing-key")], ["sig2", rsaPublicKey2.id]]) request = await signRequest(request, rsaPrivateKey2, keyId, {
1252
+ spec: "rfc9421",
1253
+ rfc9421: { label }
1254
+ });
1255
+ for (const maxHttpSignatures of [void 0, 1]) assertEquals(await createFederation({
1256
+ kv,
1257
+ documentLoaderFactory: () => mockDocumentLoader,
1258
+ contextLoaderFactory: () => mockDocumentLoader,
1259
+ maxHttpSignatures
1260
+ }).createContext(request, void 0).getSignedKey(), maxHttpSignatures == null ? rsaPublicKey2 : null);
1261
+ });
1262
+ });
1174
1263
  test("Federation.fetch()", async (t) => {
1175
1264
  esm_default.spyGlobal();
1176
1265
  esm_default.get("https://example.com/key2", {
@@ -1269,7 +1358,7 @@ test("Federation.fetch()", async (t) => {
1269
1358
  }), rsaPrivateKey2, rsaPublicKey2.id);
1270
1359
  assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
1271
1360
  assertEquals(inbox.length, 1, "Expected one item in the inbox, json");
1272
- assertEquals(inbox[0], id$2);
1361
+ assertEquals(inbox[0], create_default.id);
1273
1362
  });
1274
1363
  await t.step("GET with application/json", async () => {
1275
1364
  const { federation, dispatches } = createTestContext();
@@ -1292,7 +1381,7 @@ test("Federation.fetch()", async (t) => {
1292
1381
  }), rsaPrivateKey2, rsaPublicKey2.id);
1293
1382
  assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
1294
1383
  assertEquals(inbox.length, 1, "Expected one inbox activity, ld+json");
1295
- assertEquals(inbox[0], id$2);
1384
+ assertEquals(inbox[0], create_default.id);
1296
1385
  });
1297
1386
  await t.step("GET with application/ld+json", async () => {
1298
1387
  const { federation, dispatches } = createTestContext();
@@ -1316,7 +1405,7 @@ test("Federation.fetch()", async (t) => {
1316
1405
  }), rsaPrivateKey2, rsaPublicKey2.id);
1317
1406
  assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 202);
1318
1407
  assertEquals(inbox.length, 1);
1319
- assertEquals(inbox[0], id$2);
1408
+ assertEquals(inbox[0], create_default.id);
1320
1409
  });
1321
1410
  await t.step("GET with application/activity+json", async () => {
1322
1411
  const { federation, dispatches } = createTestContext();
@@ -2044,7 +2133,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2044
2133
  }
2045
2134
  }), { contextData: void 0 });
2046
2135
  assertEquals([response.status, await response.text()], [202, ""]);
2047
- assertEquals(received, [`john:${id$2}`]);
2136
+ assertEquals(received, [`john:${create_default.id}`]);
2048
2137
  response = await federation.fetch(new Request("https://example.com/users/no-one/outbox", {
2049
2138
  method: "POST",
2050
2139
  body: JSON.stringify(create_default),
@@ -2106,7 +2195,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2106
2195
  }
2107
2196
  }), { contextData: void 0 });
2108
2197
  assertEquals([response.status, await response.text()], [202, ""]);
2109
- assertEquals(received, [`john:${id$2}`]);
2198
+ assertEquals(received, [`john:${create_default.id}`]);
2110
2199
  });
2111
2200
  await t.step("warns when listener omits delivery", async () => {
2112
2201
  await withLogtapeLock(async () => {
@@ -2161,7 +2250,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2161
2250
  const records = [];
2162
2251
  await reset();
2163
2252
  esm_default.spyGlobal();
2164
- esm_default.post("https://remote.example/inbox", {
2253
+ esm_default.post("https://example.com/inbox", {
2165
2254
  status: 202,
2166
2255
  body: "Accepted"
2167
2256
  });
@@ -2187,7 +2276,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2187
2276
  federation.setOutboxListeners("/users/{identifier}/outbox").on(vocab.Activity, async (ctx, activity) => {
2188
2277
  await ctx.sendActivity({ identifier: ctx.identifier }, new vocab.Person({
2189
2278
  id: new URL("https://remote.example/users/alice"),
2190
- inbox: new URL("https://remote.example/inbox")
2279
+ inbox: new URL("https://example.com/inbox")
2191
2280
  }), activity);
2192
2281
  }).authorize((ctx, identifier) => {
2193
2282
  return identifier === "john" && ctx.request.headers.get("authorization") === "Bearer token";
@@ -2263,7 +2352,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2263
2352
  let ldsVerified = false;
2264
2353
  await reset();
2265
2354
  esm_default.spyGlobal();
2266
- esm_default.post("https://remote.example/inbox", async (cl) => {
2355
+ esm_default.post("https://example.com/inbox", async (cl) => {
2267
2356
  const verifyOptions = {
2268
2357
  documentLoader: mockDocumentLoader,
2269
2358
  contextLoader: mockDocumentLoader
@@ -2296,7 +2385,7 @@ test("Federation.setOutboxListeners()", async (t) => {
2296
2385
  keyId: rsaPublicKey2.id
2297
2386
  }], {
2298
2387
  id: new URL("https://remote.example/users/alice"),
2299
- inboxId: new URL("https://remote.example/inbox")
2388
+ inboxId: new URL("https://example.com/inbox")
2300
2389
  }, { skipIfUnsigned: true });
2301
2390
  }).authorize((ctx, identifier) => {
2302
2391
  return identifier === "person2" && ctx.request.headers.get("authorization") === "Bearer token";
@@ -3106,6 +3195,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
3106
3195
  const kv = new MemoryKvStore();
3107
3196
  const queuedMessages = [];
3108
3197
  const federation = new FederationImpl({
3198
+ allowPrivateAddress: true,
3109
3199
  kv,
3110
3200
  queue: {
3111
3201
  nativeRetrial: true,
@@ -3168,6 +3258,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
3168
3258
  const kv = new MemoryKvStore();
3169
3259
  let queuedMessages = [];
3170
3260
  const federation = new FederationImpl({
3261
+ allowPrivateAddress: true,
3171
3262
  kv,
3172
3263
  queue: {
3173
3264
  enqueue(message, _options) {
@@ -3238,6 +3329,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
3238
3329
  const kv = new MemoryKvStore();
3239
3330
  const [meterProvider, recorder] = createTestMeterProvider();
3240
3331
  await new FederationImpl({
3332
+ allowPrivateAddress: true,
3241
3333
  kv,
3242
3334
  meterProvider,
3243
3335
  queue: {
@@ -3280,6 +3372,7 @@ test("FederationImpl.processQueuedTask()", async (t) => {
3280
3372
  const kv = new MemoryKvStore();
3281
3373
  const [meterProvider, recorder] = createTestMeterProvider();
3282
3374
  await new FederationImpl({
3375
+ allowPrivateAddress: true,
3283
3376
  kv,
3284
3377
  meterProvider,
3285
3378
  queue: {
@@ -4225,6 +4318,51 @@ test("FederationImpl.processQueuedTask()", async (t) => {
4225
4318
  assertEquals(errorCount, 1);
4226
4319
  assertEquals(queuedMessages, []);
4227
4320
  });
4321
+ await t.step("malformed FEP-ef61 gateways are permanent queued inbox parse errors", async () => {
4322
+ const queuedMessages = [];
4323
+ const queue = {
4324
+ enqueue(message, _options) {
4325
+ queuedMessages.push(message);
4326
+ return Promise.resolve();
4327
+ },
4328
+ listen(_handler, _options) {
4329
+ return Promise.resolve();
4330
+ }
4331
+ };
4332
+ const kv = new MemoryKvStore();
4333
+ let errorCount = 0;
4334
+ const federation = new FederationImpl({
4335
+ kv,
4336
+ queue
4337
+ });
4338
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, () => {
4339
+ throw new Error("listener should not run");
4340
+ }).onError(() => {
4341
+ errorCount++;
4342
+ });
4343
+ await federation.processQueuedTask(void 0, {
4344
+ type: "inbox",
4345
+ id: crypto.randomUUID(),
4346
+ baseUrl: "https://example.com",
4347
+ activity: {
4348
+ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/fep/ef61"],
4349
+ id: "https://remote.example/activities/invalid-gateway",
4350
+ type: "Create",
4351
+ actor: "https://remote.example/users/alice",
4352
+ object: {
4353
+ id: "https://remote.example/users/bob",
4354
+ type: "Person",
4355
+ gateways: ["https://gw.example/path"]
4356
+ }
4357
+ },
4358
+ started: (/* @__PURE__ */ new Date()).toISOString(),
4359
+ attempt: 0,
4360
+ identifier: null,
4361
+ traceContext: {}
4362
+ });
4363
+ assertEquals(errorCount, 1);
4364
+ assertEquals(queuedMessages, []);
4365
+ });
4228
4366
  await t.step("legacy raw LDS inbox messages with network-path context ids retry", async () => {
4229
4367
  const queue = {
4230
4368
  enqueue(message, _options) {
@@ -4935,6 +5073,157 @@ test("FederationImpl.processQueuedTask()", async (t) => {
4935
5073
  assertEquals(queuedMessages, []);
4936
5074
  });
4937
5075
  });
5076
+ test({
5077
+ name: "FederationImpl.processQueuedTask() accounts for destination validation failures",
5078
+ ignore: "Deno" in globalThis && (await Deno.permissions.query({ name: "net" })).state !== "granted",
5079
+ async fn(t) {
5080
+ for (const result of [
5081
+ "throws",
5082
+ "empty",
5083
+ "private"
5084
+ ]) for (const failureThreshold of [1, 5]) for (const redirected of [false, true]) await t.step(`${result}, threshold: ${failureThreshold}, redirected: ${redirected}`, async () => {
5085
+ const destination = "https://delivery.invalid/inbox";
5086
+ const publicInbox = "https://8.8.8.8/inbox";
5087
+ const queuedMessages = [];
5088
+ const delays = [];
5089
+ const errors = [];
5090
+ const activities = [];
5091
+ const attempts = [];
5092
+ const delay = Temporal.Duration.from({ seconds: 5 });
5093
+ const kv = new MemoryKvStore();
5094
+ await markCircuitBreakerLegacySweepDone(kv);
5095
+ const [meterProvider, recorder] = createTestMeterProvider();
5096
+ const recoveryDelay = Temporal.Duration.from({ minutes: 30 });
5097
+ const federation = new FederationImpl({
5098
+ kv,
5099
+ meterProvider,
5100
+ circuitBreaker: {
5101
+ failureThreshold,
5102
+ recoveryDelay
5103
+ },
5104
+ queue: {
5105
+ enqueue(message, options) {
5106
+ queuedMessages.push(message);
5107
+ delays.push(options?.delay);
5108
+ return Promise.resolve();
5109
+ },
5110
+ listen() {
5111
+ return Promise.resolve();
5112
+ }
5113
+ },
5114
+ documentLoaderFactory: () => mockDocumentLoader,
5115
+ contextLoaderFactory: () => mockDocumentLoader,
5116
+ onOutboxError(error, activity) {
5117
+ errors.push(error);
5118
+ activities.push(activity);
5119
+ },
5120
+ outboxRetryPolicy(options) {
5121
+ attempts.push(options.attempts);
5122
+ return delay;
5123
+ }
5124
+ });
5125
+ const message = {
5126
+ type: "outbox",
5127
+ id: crypto.randomUUID(),
5128
+ baseUrl: "https://example.com",
5129
+ keys: [],
5130
+ activity: {
5131
+ "@context": "https://www.w3.org/ns/activitystreams",
5132
+ type: "Create",
5133
+ id: "https://example.com/activities/dns-failure",
5134
+ actor: "https://example.com/users/alice",
5135
+ object: {
5136
+ type: "Note",
5137
+ content: "test"
5138
+ }
5139
+ },
5140
+ activityType: "https://www.w3.org/ns/activitystreams#Create",
5141
+ inbox: redirected ? publicInbox : destination,
5142
+ sharedInbox: false,
5143
+ started: (/* @__PURE__ */ new Date()).toISOString(),
5144
+ attempt: 0,
5145
+ headers: {},
5146
+ traceContext: {}
5147
+ };
5148
+ const originalLookup = dns.lookup;
5149
+ const resolverError = /* @__PURE__ */ new Error("Resolver unavailable");
5150
+ const lookups = [];
5151
+ dns.lookup = ((hostname) => {
5152
+ lookups.push(hostname);
5153
+ assertEquals(hostname, "delivery.invalid");
5154
+ if (result === "throws") return Promise.reject(resolverError);
5155
+ return Promise.resolve(result === "empty" ? [] : [{
5156
+ address: "127.0.0.1",
5157
+ family: 4
5158
+ }]);
5159
+ });
5160
+ try {
5161
+ esm_default.mockGlobal().catch(202);
5162
+ if (redirected) esm_default.route(publicInbox, {
5163
+ status: 307,
5164
+ headers: { Location: destination }
5165
+ });
5166
+ await federation.processQueuedTask(void 0, message);
5167
+ assertEquals(errors.length, 1);
5168
+ const error = errors[0];
5169
+ if (result === "private") {
5170
+ assertInstanceOf(error, UrlError);
5171
+ assertEquals(error.reason, "disallowed");
5172
+ } else {
5173
+ assertInstanceOf(error, FetchError);
5174
+ assertEquals(error.url.href, destination);
5175
+ assertInstanceOf(error.cause, UrlError);
5176
+ assertEquals(error.cause.reason, "dns");
5177
+ assertStrictEquals(error.cause.cause, result === "throws" ? resolverError : void 0);
5178
+ }
5179
+ assertEquals(activities.length, 1);
5180
+ assertInstanceOf(activities[0], Create);
5181
+ assertEquals(activities[0].id?.href, "https://example.com/activities/dns-failure");
5182
+ assertEquals(attempts, [0]);
5183
+ const host = new URL(message.inbox).host;
5184
+ const state = await kv.get([
5185
+ "_fedify",
5186
+ "circuit",
5187
+ host
5188
+ ]);
5189
+ if (result === "private") assertEquals(state, void 0);
5190
+ else {
5191
+ assertExists(state);
5192
+ assertEquals(state.failures.length, 1);
5193
+ assertEquals(state.state, failureThreshold === 1 ? "open" : "closed");
5194
+ }
5195
+ if (result !== "private" && failureThreshold === 1) {
5196
+ assertEquals(queuedMessages.length, 1);
5197
+ const held = queuedMessages[0];
5198
+ assertEquals(held.attempt, 0);
5199
+ assertEquals(held.circuitHeld, true);
5200
+ assertExists(held.circuitHeldSince);
5201
+ assertEquals(delays, [recoveryDelay]);
5202
+ } else {
5203
+ assertEquals(queuedMessages, [{
5204
+ ...message,
5205
+ attempt: 1
5206
+ }]);
5207
+ assertEquals(delays, [delay]);
5208
+ }
5209
+ const expectedCount = result === "private" && !redirected ? 0 : 1;
5210
+ const sent = recorder.getMeasurements("activitypub.delivery.sent");
5211
+ assertEquals(sent.length, expectedCount);
5212
+ if (expectedCount === 1) {
5213
+ assertEquals(sent[0].value, 1);
5214
+ assertEquals(sent[0].attributes["activitypub.delivery.success"], false);
5215
+ assertEquals(sent[0].attributes["activitypub.remote.host"], host);
5216
+ }
5217
+ assertEquals(lookups, ["delivery.invalid"]);
5218
+ assertEquals(esm_default.callHistory.calls(destination).length, 0);
5219
+ assertEquals(esm_default.callHistory.calls().length, redirected ? 1 : 0);
5220
+ } finally {
5221
+ dns.lookup = originalLookup;
5222
+ esm_default.hardReset();
5223
+ }
5224
+ });
5225
+ }
5226
+ });
4938
5227
  test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
4939
5228
  esm_default.spyGlobal();
4940
5229
  esm_default.post("https://gone.example/inbox", {
@@ -4968,6 +5257,7 @@ test("FederationImpl.processQueuedTask() permanent failure", async (t) => {
4968
5257
  return Promise.resolve();
4969
5258
  }
4970
5259
  },
5260
+ allowPrivateAddress: true,
4971
5261
  ...options.permanentFailureStatusCodes ? { permanentFailureStatusCodes: options.permanentFailureStatusCodes } : {},
4972
5262
  ...options.meterProvider ? { meterProvider: options.meterProvider } : {},
4973
5263
  ...options.tracerProvider ? { tracerProvider: options.tracerProvider } : {}
@@ -5166,6 +5456,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5166
5456
  return Promise.resolve();
5167
5457
  }
5168
5458
  },
5459
+ allowPrivateAddress: true,
5169
5460
  circuitBreaker: options,
5170
5461
  ...federationOptions
5171
5462
  });
@@ -5316,6 +5607,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5316
5607
  body: ""
5317
5608
  });
5318
5609
  const { federation, queued, kv } = setup({ failureThreshold: 1 });
5610
+ await markCircuitBreakerLegacySweepDone(kv);
5319
5611
  await kv.set([
5320
5612
  "_fedify",
5321
5613
  "circuit",
@@ -5355,6 +5647,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5355
5647
  const { federation, queued, kv } = setup({ failureThreshold: 1 }, { outboxRetryPolicy: () => Temporal.Duration.from({ seconds: 3 }) });
5356
5648
  kv.cas = () => Promise.resolve(false);
5357
5649
  await federation.processQueuedTask(void 0, createOutboxMessage("https://failure-bookkeeping.example/inbox"));
5650
+ await federation.circuitBreaker?.pendingSweep;
5358
5651
  assertEquals(queued.length, 1);
5359
5652
  const retry = queued[0].message;
5360
5653
  assertEquals(retry.attempt, 1);
@@ -5449,6 +5742,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5449
5742
  body: "server error"
5450
5743
  });
5451
5744
  const { federation, queued, kv } = setup({ failureThreshold: 1 }, { permanentFailureStatusCodes: [500] });
5745
+ await markCircuitBreakerLegacySweepDone(kv);
5452
5746
  await kv.set([
5453
5747
  "_fedify",
5454
5748
  "circuit",
@@ -5681,6 +5975,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5681
5975
  failureThreshold: 1,
5682
5976
  releaseInterval: { seconds: 1 }
5683
5977
  }, { permanentFailureStatusCodes: [500] });
5978
+ await markCircuitBreakerLegacySweepDone(kv);
5684
5979
  await kv.set([
5685
5980
  "_fedify",
5686
5981
  "circuit",
@@ -5715,6 +6010,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5715
6010
  failureThreshold: 1,
5716
6011
  releaseInterval: { seconds: 1 }
5717
6012
  });
6013
+ await markCircuitBreakerLegacySweepDone(kv);
5718
6014
  await kv.set([
5719
6015
  "_fedify",
5720
6016
  "circuit",
@@ -5806,6 +6102,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5806
6102
  releaseInterval: { minutes: 1 }
5807
6103
  }
5808
6104
  });
6105
+ await markCircuitBreakerLegacySweepDone(kv);
5809
6106
  await kv.set([
5810
6107
  "_fedify",
5811
6108
  "circuit",
@@ -5848,6 +6145,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5848
6145
  recoveryDelay: { seconds: 1 }
5849
6146
  }
5850
6147
  });
6148
+ await markCircuitBreakerLegacySweepDone(kv);
5851
6149
  await kv.set([
5852
6150
  "_fedify",
5853
6151
  "circuit",
@@ -5936,6 +6234,7 @@ test("FederationImpl.processQueuedTask() circuit breaker", async (t) => {
5936
6234
  }
5937
6235
  }
5938
6236
  });
6237
+ await markCircuitBreakerLegacySweepDone(kv);
5939
6238
  await kv.set([
5940
6239
  "_fedify",
5941
6240
  "circuit",
@@ -6171,7 +6470,8 @@ test("FederationImpl.processQueuedTask() queue task metrics", async (t) => {
6171
6470
  listen(_handler, _options) {
6172
6471
  return Promise.resolve();
6173
6472
  }
6174
- }
6473
+ },
6474
+ allowPrivateAddress: true
6175
6475
  });
6176
6476
  federation.setInboxListeners("/users/{identifier}/inbox", "/inbox");
6177
6477
  esm_default.spyGlobal();
@@ -7096,6 +7396,81 @@ test({
7096
7396
  assertEquals(rejected[1].attributes["activitypub.activity.type"], "https://www.w3.org/ns/activitystreams#Offer");
7097
7397
  }
7098
7398
  });
7399
+ for (const queued of [false, true]) test(`ContextImpl.routeActivity() routes the fetched document (queued: ${queued})`, async () => {
7400
+ const id = new URL("https://example.com/verified-create");
7401
+ const genuine = new Create({
7402
+ id,
7403
+ actor: new URL("https://example.com/person"),
7404
+ object: new vocab.Note({ content: "Genuine content" }),
7405
+ to: new URL("https://example.com/recipient")
7406
+ });
7407
+ const genuineJson = await genuine.toJsonLd({ contextLoader: mockDocumentLoader });
7408
+ const forged = new Create({
7409
+ id,
7410
+ actor: new URL("https://victim.example/actor"),
7411
+ object: new vocab.Note({ content: "Forged content" }),
7412
+ to: new URL("https://attacker.example/recipient")
7413
+ });
7414
+ const messages = [];
7415
+ const queue = {
7416
+ enqueue(message) {
7417
+ messages.push(message);
7418
+ return Promise.resolve();
7419
+ },
7420
+ async listen() {}
7421
+ };
7422
+ const federation = createFederation({
7423
+ kv: new MemoryKvStore(),
7424
+ documentLoaderFactory: () => async (url, options) => url === id.href ? {
7425
+ document: genuineJson,
7426
+ documentUrl: id.href,
7427
+ contextUrl: null
7428
+ } : await mockDocumentLoader(url, options),
7429
+ contextLoaderFactory: () => mockDocumentLoader,
7430
+ queue: {
7431
+ inbox: queued ? queue : void 0,
7432
+ outbox: queue
7433
+ },
7434
+ manuallyStartQueue: true
7435
+ });
7436
+ const received = [];
7437
+ const contextDocuments = [];
7438
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(Create, async (ctx, activity) => {
7439
+ received.push(await activity.toJsonLd({ contextLoader: mockDocumentLoader }));
7440
+ assertInstanceOf(ctx, InboxContextImpl);
7441
+ contextDocuments.push(ctx.activity);
7442
+ await ctx.forwardActivity({
7443
+ privateKey: ed25519PrivateKey,
7444
+ keyId: ed25519Multikey.id
7445
+ }, {
7446
+ id: new URL("https://example.com/recipient"),
7447
+ inboxId: new URL("https://example.com/inbox")
7448
+ });
7449
+ });
7450
+ const ctx = federation.createContext(new URL("https://local.example/"));
7451
+ async function processInbox() {
7452
+ const inboxMessages = messages.filter((message) => message.type === "inbox");
7453
+ for (const message of inboxMessages) await federation.processQueuedTask(void 0, message);
7454
+ }
7455
+ assert(await ctx.routeActivity(null, forged));
7456
+ if (queued) {
7457
+ assertEquals(received, []);
7458
+ assertEquals(messages.length, 1);
7459
+ assert(messages[0].type === "inbox");
7460
+ assertEquals(messages[0].activity, genuineJson);
7461
+ await processInbox();
7462
+ }
7463
+ assertEquals(received, [genuineJson]);
7464
+ assertEquals(contextDocuments, [genuineJson]);
7465
+ const forwarded = messages.filter((message) => message.type === "outbox");
7466
+ assertEquals(forwarded.length, 1);
7467
+ assertEquals(forwarded[0].activity, genuineJson);
7468
+ assert(await ctx.routeActivity(null, genuine));
7469
+ if (queued) await processInbox();
7470
+ assertEquals(received, [genuineJson]);
7471
+ assertEquals(contextDocuments, [genuineJson]);
7472
+ assertEquals(messages.filter((message) => message.type === "outbox").length, 1);
7473
+ });
7099
7474
  test("ContextImpl.routeActivity() marks queued signed activities as non-LDS", async () => {
7100
7475
  let queuedMessage = null;
7101
7476
  const federation = new FederationImpl({
@@ -7155,6 +7530,47 @@ test("ContextImpl.getCollectionUri()", () => {
7155
7530
  assertThrows(() => ctx.getCollectionUri(Symbol(notReg), values));
7156
7531
  assertThrows(() => ctx.getCollectionUri(Symbol.for(notReg), values));
7157
7532
  });
7533
+ test("symbol-named custom collections are served and parsed", async () => {
7534
+ const federation = createFederation({ kv: new MemoryKvStore() });
7535
+ const names = [Symbol("bookmarks"), Symbol("bookmarks")];
7536
+ const paths = ["bookmarks", "ordered-bookmarks"];
7537
+ const calls = [];
7538
+ federation.setCollectionDispatcher(names[0], vocab.Object, "/users/{identifier}/bookmarks", (_ctx, _values, cursor) => {
7539
+ calls.push([0, cursor]);
7540
+ return { items: [] };
7541
+ });
7542
+ federation.setOrderedCollectionDispatcher(names[1], vocab.Object, "/users/{identifier}/ordered-bookmarks", (_ctx, _values, cursor) => {
7543
+ calls.push([1, cursor]);
7544
+ return { items: [] };
7545
+ });
7546
+ const ctx = federation.createContext(new URL("https://example.com/"));
7547
+ for (const [index, name] of names.entries()) {
7548
+ const uri = ctx.getCollectionUri(name, { identifier: "alice" });
7549
+ assertEquals(uri.pathname, `/users/alice/${paths[index]}`);
7550
+ const parsed = ctx.parseUri(uri);
7551
+ assertEquals(parsed?.type, index === 0 ? "collection" : "orderedCollection");
7552
+ if (parsed?.type !== "collection" && parsed?.type !== "orderedCollection") throw new Error("Expected a custom collection URI");
7553
+ assertStrictEquals(parsed.name, name);
7554
+ assertStrictEquals(parsed.class, vocab.Object);
7555
+ assertEquals(parsed.typeId, vocab.Object.typeId);
7556
+ assertEquals(parsed.values, { identifier: "alice" });
7557
+ for (const cursor of [null, "next"]) {
7558
+ const pageUri = new URL(uri);
7559
+ if (cursor != null) pageUri.searchParams.set("cursor", cursor);
7560
+ const response = await federation.fetch(new Request(pageUri, { headers: { accept: "application/activity+json" } }), { contextData: void 0 });
7561
+ assertEquals(response.status, 200);
7562
+ const body = await response.json();
7563
+ assert(body !== null && typeof body === "object" && "type" in body);
7564
+ assertEquals(body.type, `${index === 0 ? "Collection" : "OrderedCollection"}${cursor == null ? "" : "Page"}`);
7565
+ }
7566
+ }
7567
+ assertEquals(calls, [
7568
+ [0, null],
7569
+ [0, "next"],
7570
+ [1, null],
7571
+ [1, "next"]
7572
+ ]);
7573
+ });
7158
7574
  test("InboxContextImpl.forwardActivity()", async (t) => {
7159
7575
  esm_default.spyGlobal();
7160
7576
  let verified = null;
@@ -7348,6 +7764,193 @@ test("KvSpecDeterminer", async (t) => {
7348
7764
  spec = await determiner.determineSpec("example.com");
7349
7765
  assertEquals(spec, "rfc9421");
7350
7766
  });
7767
+ await t.step("should default specTtl to 90 days", () => {
7768
+ assertEquals(new KvSpecDeterminer(new MemoryKvStore(), ["test", "spec"]).specTtl.total("day"), 90);
7769
+ });
7770
+ await t.step("should accept a configurable specTtl as a 4th positional argument", () => {
7771
+ assertEquals(new KvSpecDeterminer(new MemoryKvStore(), ["test", "spec"], "rfc9421", { specTtl: Temporal.Duration.from({ days: 7 }) }).specTtl.total("day"), 7);
7772
+ });
7773
+ await t.step("should expire, relearn, and remember the spec again", async () => {
7774
+ const kv = new ManualClockKvStore();
7775
+ const determiner = new KvSpecDeterminer(kv, ["test", "spec"], "rfc9421", { specTtl: Temporal.Duration.from({ days: 90 }) });
7776
+ await determiner.rememberSpec("example.com", "draft-cavage-http-signatures-12");
7777
+ assertEquals(await determiner.determineSpec("example.com"), "draft-cavage-http-signatures-12");
7778
+ kv.advance({ days: 91 });
7779
+ assertEquals(await determiner.determineSpec("example.com"), "rfc9421");
7780
+ await determiner.rememberSpec("example.com", "draft-cavage-http-signatures-12");
7781
+ assertEquals(await determiner.determineSpec("example.com"), "draft-cavage-http-signatures-12");
7782
+ });
7783
+ });
7784
+ /**
7785
+ * A `KvStore` that records the TTL every write was made with, so tests can
7786
+ * assert on TTLs even after the entries themselves have expired.
7787
+ */
7788
+ var TtlRecordingKvStore = class {
7789
+ inner = new ManualClockKvStore();
7790
+ writes = [];
7791
+ /** Moves the wrapped store's virtual clock forward. */
7792
+ advance(duration) {
7793
+ this.inner.advance(duration);
7794
+ }
7795
+ get(key) {
7796
+ return this.inner.get(key);
7797
+ }
7798
+ set(key, value, options) {
7799
+ this.writes.push({
7800
+ key,
7801
+ ttl: options?.ttl
7802
+ });
7803
+ return this.inner.set(key, value, options);
7804
+ }
7805
+ delete(key) {
7806
+ return this.inner.delete(key);
7807
+ }
7808
+ list(prefix) {
7809
+ return this.inner.list(prefix);
7810
+ }
7811
+ /** The TTL of the most recent write to `key`, or `undefined` if never set. */
7812
+ lastTtl(key) {
7813
+ const matches = this.writes.filter((w) => w.key.length === key.length && w.key.every((p, i) => p === key[i]));
7814
+ return matches.length < 1 ? void 0 : matches[matches.length - 1].ttl;
7815
+ }
7816
+ };
7817
+ test("createFederation() defaults the cache TTLs and lets them be overridden", () => {
7818
+ const defaults = new FederationImpl({ kv: new MemoryKvStore() });
7819
+ assertEquals(defaults.publicKeyTtl.total("day"), 30);
7820
+ assertEquals(defaults.httpMessageSignaturesSpecTtl.total("day"), 90);
7821
+ const overridden = new FederationImpl({
7822
+ kv: new MemoryKvStore(),
7823
+ publicKeyTtl: { days: 1 },
7824
+ httpMessageSignaturesSpecTtl: { hours: 12 }
7825
+ });
7826
+ assertEquals(overridden.publicKeyTtl.total("day"), 1);
7827
+ assertEquals(overridden.httpMessageSignaturesSpecTtl.total("hour"), 12);
7828
+ });
7829
+ test("createFederation() applies httpMessageSignaturesSpecTtl to remembered specs", async () => {
7830
+ esm_default.spyGlobal();
7831
+ try {
7832
+ const attempts = [];
7833
+ esm_default.post("https://example.com/inbox", async (cl) => {
7834
+ const request = cl.request.clone();
7835
+ const spec = request.headers.has("Signature-Input") ? "rfc9421" : "draft-cavage-http-signatures-12";
7836
+ attempts.push(spec);
7837
+ if (spec === "rfc9421") return new Response(null, { status: 401 });
7838
+ const key = await verifyRequest(request, {
7839
+ documentLoader: mockDocumentLoader,
7840
+ contextLoader: mockDocumentLoader
7841
+ });
7842
+ return new Response(null, { status: key == null ? 401 : 202 });
7843
+ });
7844
+ const kv = new TtlRecordingKvStore();
7845
+ const ctx = createFederation({
7846
+ kv,
7847
+ documentLoaderFactory: () => mockDocumentLoader,
7848
+ contextLoaderFactory: () => mockDocumentLoader,
7849
+ httpMessageSignaturesSpecTtl: { days: 14 }
7850
+ }).createContext(new URL("https://example.com/"), void 0);
7851
+ const specKey = [
7852
+ "_fedify",
7853
+ "httpMessageSignaturesSpec",
7854
+ "https://example.com"
7855
+ ];
7856
+ const send = () => ctx.sendActivity([{
7857
+ privateKey: rsaPrivateKey2,
7858
+ keyId: rsaPublicKey2.id
7859
+ }], {
7860
+ id: new URL("https://example.com/recipient"),
7861
+ inboxId: new URL("https://example.com/inbox")
7862
+ }, new vocab.Create({
7863
+ id: new URL(`https://example.com/activities/${crypto.randomUUID()}`),
7864
+ actor: new URL("https://example.com/person")
7865
+ }));
7866
+ await send();
7867
+ assertEquals(attempts, ["rfc9421", "draft-cavage-http-signatures-12"]);
7868
+ assertEquals(await kv.get(specKey), "draft-cavage-http-signatures-12");
7869
+ assertEquals(kv.lastTtl(specKey)?.total("day"), 14);
7870
+ attempts.length = 0;
7871
+ await send();
7872
+ assertEquals(attempts, ["draft-cavage-http-signatures-12"]);
7873
+ kv.advance({ days: 15 });
7874
+ assertEquals(await kv.get(specKey), void 0);
7875
+ attempts.length = 0;
7876
+ await send();
7877
+ assertEquals(attempts, ["rfc9421", "draft-cavage-http-signatures-12"]);
7878
+ assertEquals(await kv.get(specKey), "draft-cavage-http-signatures-12");
7879
+ assertEquals(kv.lastTtl(specKey)?.total("day"), 14);
7880
+ } finally {
7881
+ esm_default.hardReset();
7882
+ }
7883
+ });
7884
+ test("createFederation() applies publicKeyTtl to cached public keys", async () => {
7885
+ esm_default.spyGlobal();
7886
+ try {
7887
+ let keyFetches = 0;
7888
+ esm_default.get("begin:https://example.com/person2", () => {
7889
+ keyFetches++;
7890
+ return {
7891
+ headers: { "Content-Type": "application/activity+json" },
7892
+ body: person2_default
7893
+ };
7894
+ });
7895
+ const keyId = "https://example.com/person2#key3";
7896
+ const kv = new TtlRecordingKvStore();
7897
+ const federation = createFederation({
7898
+ kv,
7899
+ documentLoaderFactory: () => mockDocumentLoader,
7900
+ contextLoaderFactory: () => mockDocumentLoader,
7901
+ publicKeyTtl: { days: 7 }
7902
+ });
7903
+ const inbox = [];
7904
+ federation.setActorDispatcher("/users/{identifier}", (_, identifier) => identifier === "john" ? new vocab.Person({}) : null).setKeyPairsDispatcher(() => [{
7905
+ privateKey: rsaPrivateKey2,
7906
+ publicKey: rsaPublicKey2.publicKey
7907
+ }]);
7908
+ federation.setInboxListeners("/users/{identifier}/inbox", "/inbox").on(vocab.Create, (_ctx, create) => {
7909
+ inbox.push(create);
7910
+ });
7911
+ const deliver = async () => {
7912
+ const activity = new vocab.Create({
7913
+ id: new URL(`https://example.com/activities/${crypto.randomUUID()}`),
7914
+ actor: new URL("https://example.com/person2")
7915
+ });
7916
+ let request = new Request("https://example.com/users/john/inbox", {
7917
+ method: "POST",
7918
+ headers: {
7919
+ "Content-Type": "application/activity+json",
7920
+ accept: "application/ld+json"
7921
+ },
7922
+ body: JSON.stringify(await activity.toJsonLd({ contextLoader: mockDocumentLoader }))
7923
+ });
7924
+ request = await signRequest(request, rsaPrivateKey3, new URL(keyId));
7925
+ return await federation.fetch(request, { contextData: void 0 });
7926
+ };
7927
+ const publicKeyKey = [
7928
+ "_fedify",
7929
+ "publicKey",
7930
+ "2",
7931
+ keyId
7932
+ ];
7933
+ assertEquals((await deliver()).status, 202);
7934
+ assertEquals(inbox.length, 1);
7935
+ assert(keyFetches > 0);
7936
+ assert(await kv.get(publicKeyKey) != null);
7937
+ assertEquals(kv.lastTtl(publicKeyKey)?.total("day"), 7);
7938
+ const keyCacheWrites = () => kv.writes.filter((w) => w.key.length === publicKeyKey.length && w.key.every((part, i) => part === publicKeyKey[i])).length;
7939
+ const warmWrites = keyCacheWrites();
7940
+ assertEquals((await deliver()).status, 202);
7941
+ assertEquals(inbox.length, 2);
7942
+ assertEquals(keyCacheWrites(), warmWrites);
7943
+ kv.advance({ days: 8 });
7944
+ assertEquals(await kv.get(publicKeyKey), void 0);
7945
+ keyFetches = 0;
7946
+ assertEquals((await deliver()).status, 202);
7947
+ assertEquals(inbox.length, 3);
7948
+ assert(keyFetches > 0);
7949
+ assert(await kv.get(publicKeyKey) != null);
7950
+ assertEquals(kv.lastTtl(publicKeyKey)?.total("day"), 7);
7951
+ } finally {
7952
+ esm_default.hardReset();
7953
+ }
7351
7954
  });
7352
7955
  test("createFederation() instruments documentLoader with activitypub.document.fetch", async () => {
7353
7956
  const [meterProvider, recorder] = createTestMeterProvider();
@@ -7589,5 +8192,334 @@ test("ContextImpl.enqueueTaskMany()", async (t) => {
7589
8192
  strictEqual(queue.enqueued.length, 0);
7590
8193
  });
7591
8194
  });
8195
+ test("ContextImpl.sendActivity() honors the private-address policy", async (t) => {
8196
+ for (const allowPrivateAddress of [false, true]) await t.step(`allowPrivateAddress: ${allowPrivateAddress}`, async () => {
8197
+ const inbox = "http://127.0.0.1/inbox";
8198
+ esm_default.mockGlobal().post(inbox, 202);
8199
+ try {
8200
+ const ctx = createFederation({
8201
+ kv: new MemoryKvStore(),
8202
+ allowPrivateAddress
8203
+ }).createContext(new URL("https://example.com/"));
8204
+ const send = () => ctx.sendActivity({
8205
+ privateKey: ed25519PrivateKey,
8206
+ keyId: ed25519Multikey.id
8207
+ }, new Person({
8208
+ id: new URL("https://example.com/recipient"),
8209
+ inbox: new URL(inbox)
8210
+ }), new Create({
8211
+ id: new URL("https://example.com/activity"),
8212
+ actor: new URL("https://example.com/person")
8213
+ }), { immediate: true });
8214
+ if (allowPrivateAddress) {
8215
+ await send();
8216
+ assertEquals(esm_default.callHistory.calls(inbox).length, 1);
8217
+ } else {
8218
+ await assertRejects(send, UrlError);
8219
+ assertEquals(esm_default.callHistory.calls(inbox).length, 0);
8220
+ }
8221
+ } finally {
8222
+ esm_default.hardReset();
8223
+ }
8224
+ });
8225
+ });
8226
+ test("Federation.fetch() bounds inbox bodies before dispatch", async () => {
8227
+ let dispatched = false;
8228
+ const json = JSON.stringify({
8229
+ "@context": "https://www.w3.org/ns/activitystreams",
8230
+ id: "https://example.com/activities/bounded",
8231
+ type: "Create",
8232
+ actor: "https://example.com/actor"
8233
+ });
8234
+ const federation = createFederation({
8235
+ kv: new MemoryKvStore(),
8236
+ skipSignatureVerification: true,
8237
+ contextLoaderFactory: () => mockDocumentLoader
8238
+ });
8239
+ federation.setActorDispatcher("/actors/{identifier}", () => new Person({ id: new URL("https://example.com/actor") }));
8240
+ federation.setInboxListeners("/actors/{identifier}/inbox", "/inbox").on(Create, () => {
8241
+ dispatched = true;
8242
+ });
8243
+ for (const contentLength of [
8244
+ void 0,
8245
+ "1",
8246
+ String(16777217)
8247
+ ]) {
8248
+ assertEquals((await federation.fetch(new Request("https://example.com/inbox", {
8249
+ method: "POST",
8250
+ headers: contentLength == null ? {} : { "Content-Length": contentLength },
8251
+ body: json.padEnd(16777217)
8252
+ }), { contextData: void 0 })).status, 413);
8253
+ assertFalse(dispatched);
8254
+ }
8255
+ let canceled = false;
8256
+ let pulls = 0;
8257
+ const body = new ReadableStream({
8258
+ pull(controller) {
8259
+ pulls++;
8260
+ controller.enqueue(new Uint8Array(64 * 1024));
8261
+ },
8262
+ cancel() {
8263
+ canceled = true;
8264
+ }
8265
+ }, { highWaterMark: 0 });
8266
+ const request = new Request("https://example.com/inbox", {
8267
+ method: "POST",
8268
+ body,
8269
+ duplex: "half"
8270
+ });
8271
+ assertEquals((await federation.fetch(request, { contextData: void 0 })).status, 413);
8272
+ await new Promise((resolve) => setTimeout(resolve, 0));
8273
+ assert(canceled);
8274
+ assert(pulls <= 260);
8275
+ assertFalse(dispatched);
8276
+ assertEquals((await federation.fetch(new Request("https://example.com/inbox", {
8277
+ method: "POST",
8278
+ body: json
8279
+ }), { contextData: void 0 })).status, 202);
8280
+ assert(dispatched);
8281
+ });
8282
+ function getRawMessage(record) {
8283
+ return typeof record.rawMessage === "string" ? record.rawMessage : record.rawMessage.join("");
8284
+ }
8285
+ test("Federation.fetch() serves tombstones of objects", async (t) => {
8286
+ const deleted = Temporal.Instant.from("2024-01-15T00:00:00Z");
8287
+ const federation = createFederation({
8288
+ kv: new MemoryKvStore(),
8289
+ documentLoaderFactory: () => mockDocumentLoader,
8290
+ contextLoaderFactory: () => mockDocumentLoader
8291
+ });
8292
+ federation.setObjectDispatcher(vocab.Note, "/users/{identifier}/notes/{id}", (ctx, values) => {
8293
+ if (values.id === "missing") return null;
8294
+ if (values.id === "deleted") return new vocab.Tombstone({
8295
+ id: ctx.getObjectUri(vocab.Note, values),
8296
+ formerType: vocab.Note,
8297
+ deleted
8298
+ });
8299
+ return new vocab.Note({
8300
+ id: ctx.getObjectUri(vocab.Note, values),
8301
+ content: "Hello"
8302
+ });
8303
+ });
8304
+ federation.setObjectDispatcher(vocab.Tombstone, "/tombstones/{id}", (ctx, values) => new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Tombstone, values) }));
8305
+ federation.setObjectDispatcher(vocab.Object, "/objects/{id}", (ctx, values) => values.id === "deleted" ? new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Object, values) }) : new vocab.Note({ id: ctx.getObjectUri(vocab.Object, values) }));
8306
+ function fetch(path, method = "GET") {
8307
+ return federation.fetch(new Request(`https://example.com${path}`, {
8308
+ method,
8309
+ headers: { Accept: "application/activity+json" }
8310
+ }), { contextData: void 0 });
8311
+ }
8312
+ await t.step("GET responds with 410 Gone and the tombstone", async () => {
8313
+ const response = await fetch("/users/john/notes/deleted");
8314
+ assertEquals(response.status, 410);
8315
+ assertEquals(response.headers.get("Content-Type"), "application/activity+json");
8316
+ const body = await response.json();
8317
+ assertEquals(body.id, "https://example.com/users/john/notes/deleted");
8318
+ assertEquals(body.type, "Tombstone");
8319
+ assertEquals(body.formerType, "as:Note");
8320
+ assertEquals(body.deleted, "2024-01-15T00:00:00Z");
8321
+ });
8322
+ await t.step("HEAD responds with 410 Gone without a body", async () => {
8323
+ const response = await fetch("/users/john/notes/deleted", "HEAD");
8324
+ assertEquals(response.status, 410);
8325
+ assertEquals(response.body, null);
8326
+ });
8327
+ await t.step("objects and null are served as before", async () => {
8328
+ const response = await fetch("/users/john/notes/123");
8329
+ assertEquals(response.status, 200);
8330
+ assertEquals((await response.json()).type, "Note");
8331
+ assertEquals((await fetch("/users/john/notes/missing")).status, 404);
8332
+ });
8333
+ await t.step("Tombstone and Object dispatchers respond with 410", async () => {
8334
+ assertEquals((await fetch("/tombstones/1")).status, 410);
8335
+ assertEquals((await fetch("/objects/deleted")).status, 410);
8336
+ assertEquals((await fetch("/objects/live")).status, 200);
8337
+ });
8338
+ await t.step("RequestContext.getObject()", async () => {
8339
+ const ctx = federation.createContext(new Request("https://example.com/"), void 0);
8340
+ const values = {
8341
+ identifier: "john",
8342
+ id: "deleted"
8343
+ };
8344
+ assertEquals(await ctx.getObject(vocab.Note, values), null);
8345
+ assertEquals(await ctx.getObject(vocab.Note, values, { tombstone: "suppress" }), null);
8346
+ assertEquals(await ctx.getObject(vocab.Note, values, {}), null);
8347
+ const tombstone = await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" });
8348
+ assertInstanceOf(tombstone, vocab.Tombstone);
8349
+ assertEquals(tombstone.id, new URL("https://example.com/users/john/notes/deleted"));
8350
+ assertEquals(tombstone.deleted, deleted);
8351
+ assertInstanceOf(await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" }), vocab.Tombstone);
8352
+ assertInstanceOf(await ctx.getObject(vocab.Note, {
8353
+ identifier: "john",
8354
+ id: "123"
8355
+ }), vocab.Note);
8356
+ assertEquals(await ctx.getObject(vocab.Note, {
8357
+ identifier: "john",
8358
+ id: "missing"
8359
+ }, { tombstone: "passthrough" }), null);
8360
+ assertInstanceOf(await ctx.getObject(vocab.Tombstone, { id: "1" }), vocab.Tombstone);
8361
+ assertInstanceOf(await ctx.getObject(vocab.Object, { id: "deleted" }, { tombstone: "suppress" }), vocab.Tombstone);
8362
+ });
8363
+ await t.step("authorization predicates can get the tombstone", async () => {
8364
+ await withLogtapeLock(async () => {
8365
+ const records = [];
8366
+ await reset();
8367
+ try {
8368
+ await configure({
8369
+ sinks: { buffer(record) {
8370
+ records.push(record);
8371
+ } },
8372
+ filters: {},
8373
+ loggers: [{
8374
+ category: ["fedify", "federation"],
8375
+ sinks: ["buffer"]
8376
+ }, {
8377
+ category: ["logtape", "meta"],
8378
+ sinks: []
8379
+ }]
8380
+ });
8381
+ const federation = createFederation({
8382
+ kv: new MemoryKvStore(),
8383
+ documentLoaderFactory: () => mockDocumentLoader,
8384
+ contextLoaderFactory: () => mockDocumentLoader
8385
+ });
8386
+ let dispatched = 0;
8387
+ federation.setObjectDispatcher(vocab.Note, "/notes/{id}", (ctx, values) => {
8388
+ dispatched++;
8389
+ return new vocab.Tombstone({ id: ctx.getObjectUri(vocab.Note, values) });
8390
+ }).authorize(async (ctx, values) => {
8391
+ return await ctx.getObject(vocab.Note, values, { tombstone: "passthrough" }) instanceof vocab.Tombstone;
8392
+ });
8393
+ assertEquals((await federation.fetch(new Request("https://example.com/notes/1", { headers: { Accept: "application/activity+json" } }), { contextData: void 0 })).status, 410);
8394
+ assertEquals(dispatched, 2);
8395
+ assert(records.some((record) => getRawMessage(record).startsWith("RequestContext.getObject(") && getRawMessage(record).includes("may cause an infinite loop")));
8396
+ } finally {
8397
+ await reset();
8398
+ }
8399
+ });
8400
+ });
8401
+ await t.step("warns about the id of a tombstone", async () => {
8402
+ await withLogtapeLock(async () => {
8403
+ const records = [];
8404
+ await reset();
8405
+ try {
8406
+ await configure({
8407
+ sinks: { buffer(record) {
8408
+ records.push(record);
8409
+ } },
8410
+ filters: {},
8411
+ loggers: [{
8412
+ category: [
8413
+ "fedify",
8414
+ "federation",
8415
+ "object"
8416
+ ],
8417
+ sinks: ["buffer"]
8418
+ }, {
8419
+ category: ["logtape", "meta"],
8420
+ sinks: []
8421
+ }]
8422
+ });
8423
+ const federation = createFederation({
8424
+ kv: new MemoryKvStore(),
8425
+ documentLoaderFactory: () => mockDocumentLoader,
8426
+ contextLoaderFactory: () => mockDocumentLoader
8427
+ });
8428
+ const ids = {
8429
+ "matching": new URL("https://example.com/notes/matching"),
8430
+ "missing": null,
8431
+ "mismatched": new URL("https://example.com/other/mismatched"),
8432
+ "ap": new URL("ap://did%3Akey%3Az6MkTest/notes/ap"),
8433
+ "ap-ef61": new URL("ap+ef61://did%3Akey%3Az6MkTest/notes/ap-ef61")
8434
+ };
8435
+ federation.setObjectDispatcher(vocab.Note, "/notes/{id}", (_ctx, { id }) => id === "live" ? new vocab.Note({ id: new URL("https://example.com/other/live") }) : new vocab.Tombstone({ id: ids[id] }));
8436
+ const warnings = async (id) => {
8437
+ records.length = 0;
8438
+ assertEquals((await federation.fetch(new Request(`https://example.com/notes/${id}`, { headers: { Accept: "application/activity+json" } }), { contextData: void 0 })).status, id === "live" ? 200 : 410);
8439
+ return records.filter((r) => r.level === "warning");
8440
+ };
8441
+ assertEquals(await warnings("matching"), []);
8442
+ const missing = await warnings("missing");
8443
+ assertEquals(missing.length, 1);
8444
+ assert(getRawMessage(missing[0]).includes("without an id property"));
8445
+ const mismatched = await warnings("mismatched");
8446
+ assertEquals(mismatched.length, 1);
8447
+ assert(getRawMessage(mismatched[0]).includes("does not match"));
8448
+ assertEquals(mismatched[0].properties.tombstoneId, "https://example.com/other/mismatched");
8449
+ assertEquals(mismatched[0].properties.objectUri, "https://example.com/notes/mismatched");
8450
+ assertEquals(await warnings("ap"), []);
8451
+ assertEquals(await warnings("ap-ef61"), []);
8452
+ assertEquals(await warnings("live"), []);
8453
+ } finally {
8454
+ await reset();
8455
+ }
8456
+ });
8457
+ });
8458
+ });
8459
+ test("createFederation() applies documentLoaderTimeout to built-in loaders", async (t) => {
8460
+ esm_default.mockGlobal();
8461
+ let requests = 0;
8462
+ esm_default.get("begin:https://slow.example/", () => {
8463
+ requests++;
8464
+ return new Promise(() => {});
8465
+ });
8466
+ const isTimeout = (error) => {
8467
+ ok(error instanceof FetchError, String(error));
8468
+ strictEqual(error.response, void 0);
8469
+ ok(error.cause instanceof DOMException);
8470
+ strictEqual(error.cause.name, "TimeoutError");
8471
+ ok(error.message.endsWith("Timed out after 100 ms"), error.message);
8472
+ return true;
8473
+ };
8474
+ try {
8475
+ await t.step("built-in loaders", async () => {
8476
+ const ctx = createFederation({
8477
+ kv: new MemoryKvStore(),
8478
+ allowPrivateAddress: true,
8479
+ documentLoaderTimeout: { milliseconds: 100 }
8480
+ }).createContext(new URL("https://example.com/"), void 0);
8481
+ await rejects(ctx.documentLoader("https://slow.example/object"), isTimeout);
8482
+ await rejects(ctx.contextLoader("https://slow.example/context"), isTimeout);
8483
+ await rejects(ctx.getDocumentLoader({
8484
+ keyId: new URL("https://example.com/key2"),
8485
+ privateKey: rsaPrivateKey2
8486
+ })("https://slow.example/private"), isTimeout);
8487
+ strictEqual(requests, 3);
8488
+ });
8489
+ await t.step("per-call override and null", async () => {
8490
+ const federation = createFederation({
8491
+ kv: new MemoryKvStore(),
8492
+ allowPrivateAddress: true,
8493
+ documentLoaderTimeout: null
8494
+ });
8495
+ await rejects(federation.documentLoaderFactory({ timeout: 100 })("https://slow.example/object"), isTimeout);
8496
+ await rejects(federation.authenticatedDocumentLoaderFactory({
8497
+ keyId: new URL("https://example.com/key2"),
8498
+ privateKey: rsaPrivateKey2
8499
+ }, { timeout: 100 })("https://slow.example/private"), isTimeout);
8500
+ const controller = new AbortController();
8501
+ const reason = /* @__PURE__ */ new Error("Canceled by the caller");
8502
+ setTimeout(() => controller.abort(reason), 150);
8503
+ await rejects(federation.documentLoaderFactory()("https://slow.example/object", { signal: controller.signal }), (error) => error === reason || error instanceof Error && error.name === "AbortError");
8504
+ });
8505
+ } finally {
8506
+ esm_default.hardReset();
8507
+ }
8508
+ });
8509
+ test("createFederation() validates documentLoaderTimeout", () => {
8510
+ for (const documentLoaderTimeout of [
8511
+ { milliseconds: 0 },
8512
+ { seconds: -1 },
8513
+ { days: 25 },
8514
+ { months: 1 }
8515
+ ]) throws(() => createFederation({
8516
+ kv: new MemoryKvStore(),
8517
+ documentLoaderTimeout
8518
+ }), RangeError);
8519
+ createFederation({
8520
+ kv: new MemoryKvStore(),
8521
+ documentLoaderTimeout: Temporal.Duration.from({ seconds: 30 })
8522
+ });
8523
+ });
7592
8524
  //#endregion
7593
8525
  export {};