@fedify/fedify 2.4.0-pr.936.41 → 2.4.1

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-C1o3n_ih.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-DoFNbS9M.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-D7MEaAg-.mjs} +1 -1
  34. package/dist/dist-DJQM4u7i.mjs +189 -0
  35. package/dist/docloader-uYfrT5sv.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 +246 -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-4rvjH8Md.cjs} +8663 -3725
  92. package/dist/http-B_wV17Ij.js +6527 -0
  93. package/dist/{http-CPVMUgsW.mjs → http-CNzz4a_O.mjs} +397 -168
  94. package/dist/http-riIBL3-C.cjs +10630 -0
  95. package/dist/key-C1nv7_jD.d.ts +1125 -0
  96. package/dist/key-DYxuyd-s.d.cts +1125 -0
  97. package/dist/key-hMoNKe7p.mjs +951 -0
  98. package/dist/key-owner-evidence-APB6NpSk.mjs +108 -0
  99. package/dist/keycache-D-MNltOk.mjs +221 -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-DQ29C4XL.cjs → kv-cache-BjK2Y9Wu.cjs} +55 -19
  104. package/dist/{kv-cache-KyGqJqSp.mjs → kv-cache-CetcQsQa.mjs} +1 -1
  105. package/dist/{kv-cache-5bnX3-Jq.js → kv-cache-DVJLXeI0.js} +54 -16
  106. package/dist/{ld-Cj9F_5AP.mjs → ld-CW_8GOEC.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-CBugc7uM.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-CvwTUgnP.mjs → middleware-CEQBUZaH.mjs} +3157 -272
  113. package/dist/{middleware-Y-kpsp6S.js → middleware-CjdyJcGd.js} +5530 -1120
  114. package/dist/{middleware-CEctxBN6.mjs → middleware-D0PeYTxA.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--VRABJfK.mjs +267 -0
  139. package/dist/{owner-DEvZuyOE.d.cts → owner-BF0L9cMt.d.cts} +11 -0
  140. package/dist/{owner-CnngXDNJ.d.ts → owner-BSzr5-I8.d.ts} +11 -0
  141. package/dist/portable-collection-BVQYFfEq.mjs +418 -0
  142. package/dist/portable-collection-CiKz-t9W.cjs +686 -0
  143. package/dist/portable-collection-CnSD193J.js +670 -0
  144. package/dist/portable-key-BjUXSULt.mjs +549 -0
  145. package/dist/portable-key-id-Ca30-hOa.mjs +248 -0
  146. package/dist/proof-kTlg91Kb.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-DohrDmGq.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 +759 -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-VKyzJSNt.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,18 +1,47 @@
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 assertEquals } from "../assert_equals-BV7oLMxm.mjs";
6
- import { r as assertExists, t as assertStringIncludes } from "../std__assert-B2NnY9bq.mjs";
7
- import { n as assertGreaterOrEqual, r as assertFalse, t as assertRejects } from "../assert_rejects-EgstcQy5.mjs";
8
- import { t as assertThrows } from "../assert_throws-NiFfqbnH.mjs";
9
- import { t as assert } from "../assert-CJ1KXcvv.mjs";
10
- import { t as exportJwk } from "../key-DfPsX8hv.mjs";
11
- import { a as parseRfc9421Signature, c as timingSafeEqual, i as formatRfc9421SignatureParameters, l as verifyRequest, n as doubleKnock, o as parseRfc9421SignatureInput, r as formatRfc9421Signature, s as signRequest, t as createRfc9421SignatureBase, u as verifyRequestDetailed } from "../http-CPVMUgsW.mjs";
12
- import { i as rsaPrivateKey2, l as rsaPublicKey5, o as rsaPublicKey1, s as rsaPublicKey2 } from "../keys-Dbyd7bhl.mjs";
4
+ import { t as esm_default } from "../esm-BHXXx4Wx.mjs";
5
+ import { t as MemoryKvStore } from "../kv-DCHkaJ9-.mjs";
6
+ import { t as AssertionError } from "../assertion_error-B-cx0fFA.mjs";
7
+ import { t as assertEquals } from "../assert_equals-Z6IcEbWU.mjs";
8
+ import { t as assertExists } from "../assert_exists-cffSpJ7X.mjs";
9
+ import { t as assertFalse } from "../assert_false-CPdRnc3E.mjs";
10
+ import { t as assertGreaterOrEqual } from "../assert_greater_or_equal-BlafOF4-.mjs";
11
+ import { t as assertThrows } from "../assert_throws-BYITOvPO.mjs";
12
+ import "../std__assert-DvMDRPes.mjs";
13
+ import { t as assertRejects } from "../assert_rejects-CTptdYck.mjs";
14
+ import { t as assert } from "../assert-D1Eafmd_.mjs";
15
+ import { n as exportJwk } from "../key-hMoNKe7p.mjs";
16
+ import { a as listRequestSignatures, c as selectRequestSignature, f as verifyRequest, i as formatRfc9421SignatureParameters, l as signRequest, n as doubleKnock, o as parseRfc9421Signature, p as verifyRequestDetailed, r as formatRfc9421Signature, s as parseRfc9421SignatureInput, t as createRfc9421SignatureBase, u as timingSafeEqual } from "../http-CNzz4a_O.mjs";
17
+ import { a as rsaPrivateKey3, i as rsaPrivateKey2, l as rsaPublicKey5, n as ed25519PrivateKey, o as rsaPublicKey1, r as ed25519PublicKey, s as rsaPublicKey2 } from "../keys-DIJNHW-Y.mjs";
18
+ import { t as KvKeyCache } from "../keycache-D-MNltOk.mjs";
19
+ import { FetchError, UrlError, exportSpki, getDocumentLoader } from "@fedify/vocab-runtime";
13
20
  import { createTestMeterProvider, createTestTracerProvider, mockDocumentLoader, test } from "@fedify/fixture";
14
- import { FetchError, exportSpki } from "@fedify/vocab-runtime";
15
21
  import { encodeBase64 } from "byte-encodings/base64";
22
+ //#region ../../node_modules/.pnpm/@jsr+std__assert@0.226.0/node_modules/@jsr/std__assert/assert_string_includes.js
23
+ /**
24
+ * Make an assertion that actual includes expected. If not
25
+ * then throw.
26
+ *
27
+ * @example Usage
28
+ * ```ts no-eval
29
+ * import { assertStringIncludes } from "@std/assert/assert-string-includes";
30
+ *
31
+ * assertStringIncludes("Hello", "ello"); // Doesn't throw
32
+ * assertStringIncludes("Hello", "world"); // Throws
33
+ * ```
34
+ *
35
+ * @param actual The actual string to check for inclusion.
36
+ * @param expected The expected string to check for inclusion.
37
+ * @param msg The optional message to display if the assertion fails.
38
+ */ function assertStringIncludes(actual, expected, msg) {
39
+ if (!actual.includes(expected)) {
40
+ msg = `Expected actual: "${actual}" to contain: "${expected}"${msg ? `: ${msg}` : "."}`;
41
+ throw new AssertionError(msg);
42
+ }
43
+ }
44
+ //#endregion
16
45
  //#region src/sig/http.test.ts
17
46
  test("signRequest() [draft-cavage]", async () => {
18
47
  assertEquals(await verifyRequest(await signRequest(new Request("https://example.com/", {
@@ -231,7 +260,7 @@ test("verifyRequestDetailed() records failure details on span", async () => {
231
260
  assertEquals(span.attributes["http_signatures.key_fetch_status"], 410);
232
261
  });
233
262
  test("verifyRequestDetailed() records verification duration metric", async (t) => {
234
- const buildSignedRequest = () => signRequest(new Request("https://example.com/inbox", {
263
+ const buildSignedRequest = (spec) => signRequest(new Request("https://example.com/inbox", {
235
264
  method: "POST",
236
265
  headers: {
237
266
  "Content-Type": "application/activity+json",
@@ -242,7 +271,7 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
242
271
  type: "Create",
243
272
  actor: "https://example.com/key2"
244
273
  })
245
- }), rsaPrivateKey2, new URL("https://example.com/key2"));
274
+ }), rsaPrivateKey2, new URL("https://example.com/key2"), { spec });
246
275
  await t.step("verified path emits one measurement", async () => {
247
276
  const [meterProvider, recorder] = createTestMeterProvider();
248
277
  assert((await verifyRequestDetailed(await buildSignedRequest(), {
@@ -345,23 +374,26 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
345
374
  assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1);
346
375
  });
347
376
  await t.step("cached-key retry emits one measurement, not two", async () => {
348
- const [meterProvider, recorder] = createTestMeterProvider();
349
- const cache = { "https://example.com/key2": rsaPublicKey1 };
350
- assertExists(await verifyRequest(await buildSignedRequest(), {
351
- contextLoader: mockDocumentLoader,
352
- documentLoader: mockDocumentLoader,
353
- meterProvider,
354
- keyCache: {
355
- get(keyId) {
356
- return Promise.resolve(cache[keyId.href]);
357
- },
358
- set(keyId, k) {
359
- cache[keyId.href] = k;
360
- return Promise.resolve();
377
+ for (const spec of ["draft-cavage-http-signatures-12", "rfc9421"]) {
378
+ const [meterProvider, recorder] = createTestMeterProvider();
379
+ const cache = { "https://example.com/key2": rsaPublicKey1 };
380
+ assertExists(await verifyRequest(await buildSignedRequest(spec), {
381
+ contextLoader: mockDocumentLoader,
382
+ documentLoader: mockDocumentLoader,
383
+ meterProvider,
384
+ keyCache: {
385
+ get(keyId) {
386
+ return Promise.resolve(cache[keyId.href]);
387
+ },
388
+ set(keyId, k) {
389
+ cache[keyId.href] = k;
390
+ return Promise.resolve();
391
+ }
361
392
  }
362
- }
363
- }));
364
- assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1);
393
+ }), spec);
394
+ assertEquals(recorder.getMeasurements("activitypub.signature.verification.duration").length, 1, spec);
395
+ assertEquals(recorder.getMeasurements("activitypub.signature.key_fetch.duration").length, 2, spec);
396
+ }
365
397
  });
366
398
  await t.step("key fetch records result=fetched on a cold cache", async () => {
367
399
  const [meterProvider, recorder] = createTestMeterProvider();
@@ -440,6 +472,63 @@ test("verifyRequestDetailed() records verification duration metric", async (t) =
440
472
  assertFalse("http_signatures.algorithm" in measurements[0].attributes);
441
473
  });
442
474
  });
475
+ test("verifyRequestDetailed() preserves cached key fetch errors", async (t) => {
476
+ const keyId = new URL("https://unreachable.example/actors/alice#main-key");
477
+ const timeout = new FetchError("https://unreachable.example/resolved-key", "Timed out after 100 ms");
478
+ timeout.cause = new DOMException("The operation timed out", "TimeoutError");
479
+ const failures = [
480
+ timeout,
481
+ new UrlError("DNS lookup failed", {
482
+ reason: "dns",
483
+ cause: /* @__PURE__ */ new TypeError("No usable IP addresses")
484
+ }),
485
+ new UrlError("Private IP address is not allowed")
486
+ ];
487
+ for (const failure of failures) await t.step(failure.message, async () => {
488
+ const kv = new MemoryKvStore();
489
+ let fetches = 0;
490
+ const documentLoader = (url) => {
491
+ if (url === keyId.href) {
492
+ fetches++;
493
+ throw failure;
494
+ }
495
+ return mockDocumentLoader(url);
496
+ };
497
+ for (const cached of [false, true]) {
498
+ const result = await verifyRequestDetailed(await signRequest(new Request("https://example.com/inbox", {
499
+ method: "POST",
500
+ body: "Test activity"
501
+ }), rsaPrivateKey2, keyId), {
502
+ documentLoader,
503
+ contextLoader: mockDocumentLoader,
504
+ keyCache: new KvKeyCache(kv, ["pk"])
505
+ });
506
+ assertFalse(result.verified);
507
+ assert(result.reason.type === "keyFetchError");
508
+ assert("error" in result.reason.result);
509
+ const error = result.reason.result.error;
510
+ assertEquals(error.name, failure.name);
511
+ assertEquals(error.message, failure.message);
512
+ if (failure instanceof FetchError) {
513
+ assert(error instanceof FetchError);
514
+ assertEquals(error.url.href, failure.url.href);
515
+ assertEquals(error.response, void 0);
516
+ assert(error.cause instanceof DOMException);
517
+ assertEquals(error.cause.name, "TimeoutError");
518
+ assertEquals(error.cause.message, "The operation timed out");
519
+ } else {
520
+ assert(error instanceof UrlError);
521
+ assertEquals(error.reason, failure.reason);
522
+ if (failure.cause instanceof Error) {
523
+ assert(error.cause instanceof Error);
524
+ assertEquals(error.cause.name, failure.cause.name);
525
+ assertEquals(error.cause.message, failure.cause.message);
526
+ } else assertEquals(error.cause, void 0);
527
+ }
528
+ assertEquals(fetches, 1, cached ? "Cached failure was refetched" : "");
529
+ }
530
+ });
531
+ });
443
532
  test("signRequest() and verifyRequest() [rfc9421] implementation", async () => {
444
533
  const currentTimestamp = 1709626184;
445
534
  const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
@@ -628,6 +717,430 @@ test("verifyRequest() [rfc9421] successful GET verification", async () => {
628
717
  currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
629
718
  }), rsaPublicKey2, "Valid signature should verify to the correct public key");
630
719
  });
720
+ for (const failure of [
721
+ "missing key",
722
+ "invalid signature",
723
+ "digest mismatch",
724
+ "empty body",
725
+ "cached key"
726
+ ]) test(`verifyRequest() [rfc9421] multiple POST signatures: ${failure}`, async () => {
727
+ const body = failure === "empty body" ? "" : "Hello, world!";
728
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
729
+ const signed = await signRequest(new Request("https://example.com/inbox", {
730
+ method: "POST",
731
+ body
732
+ }), rsaPrivateKey2, new URL("https://example.com/key2"), {
733
+ spec: "rfc9421",
734
+ currentTime
735
+ });
736
+ const input = signed.headers.get("Signature-Input");
737
+ const signature = signed.headers.get("Signature");
738
+ const firstInput = failure === "missing key" ? input.replace("https://example.com/key2", "https://example.com/missing-key") : input;
739
+ signed.headers.set("Signature-Input", `${firstInput}, ${input.replace(/^sig1=/, "sig2=")}`);
740
+ signed.headers.set("Signature", `sig1=:AAAAAA==:, ${signature.replace(/^sig1=/, "sig2=")}`);
741
+ const request = failure === "digest mismatch" ? new Request(signed, { body: "Tampered body" }) : signed;
742
+ const loaded = [];
743
+ assertEquals(await verifyRequest(request, {
744
+ keyCache: failure === "cached key" ? {
745
+ get: () => Promise.resolve(rsaPublicKey1),
746
+ set: async () => {}
747
+ } : void 0,
748
+ spec: "rfc9421",
749
+ currentTime,
750
+ contextLoader: mockDocumentLoader,
751
+ documentLoader: (url, options) => {
752
+ loaded.push(url);
753
+ return mockDocumentLoader(url, options);
754
+ }
755
+ }), failure === "digest mismatch" ? null : rsaPublicKey2);
756
+ assertEquals(loaded, failure === "digest mismatch" ? [] : failure === "missing key" ? ["https://example.com/missing-key", "https://example.com/key2"] : ["https://example.com/key2"]);
757
+ assertFalse(request.bodyUsed);
758
+ assertEquals(await request.text(), failure === "digest mismatch" ? "Tampered body" : body);
759
+ });
760
+ async function signRfc9421Many(signatures, currentTime) {
761
+ let request = new Request("https://example.com/inbox", {
762
+ method: "POST",
763
+ body: "Hello, world!"
764
+ });
765
+ let i = 0;
766
+ for (const signature of signatures) {
767
+ const { label, keyId, privateKey } = typeof signature === "string" ? { keyId: signature } : signature;
768
+ i++;
769
+ request = await signRequest(request, privateKey ?? rsaPrivateKey2, new URL(keyId), {
770
+ spec: "rfc9421",
771
+ currentTime,
772
+ rfc9421: { label: label ?? `sig${i}` }
773
+ });
774
+ }
775
+ return request;
776
+ }
777
+ function splitRfc9421Header(header) {
778
+ return Object.fromEntries(header.split(/,\s*/).map((member) => [member.slice(0, member.indexOf("=")), member]));
779
+ }
780
+ function createCountingKeyLoader() {
781
+ const loaded = [];
782
+ const loader = (url, options) => {
783
+ loaded.push(url);
784
+ if (url === "https://example.com/key2") return mockDocumentLoader(url, options);
785
+ return Promise.reject(new FetchError(url, `Not found: ${url}`));
786
+ };
787
+ return Object.assign(loader, { loaded });
788
+ }
789
+ test("verifyRequestDetailed() [rfc9421] caps the number of signatures", async (t) => {
790
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
791
+ const missing = (n) => `https://example.com/missing-${n}`;
792
+ const valid = "https://example.com/key2";
793
+ await t.step("fetches only the keys of the first signatures", async () => {
794
+ const request = await signRfc9421Many([
795
+ 1,
796
+ 2,
797
+ 3,
798
+ 4,
799
+ 5
800
+ ].map(missing), currentTime);
801
+ for (const [maxSignatures, count] of [
802
+ [void 0, 3],
803
+ [1, 1],
804
+ [5, 5],
805
+ [Infinity, 5]
806
+ ]) {
807
+ const documentLoader = createCountingKeyLoader();
808
+ const result = await verifyRequestDetailed(request, {
809
+ documentLoader,
810
+ contextLoader: mockDocumentLoader,
811
+ currentTime,
812
+ maxSignatures
813
+ });
814
+ assertFalse(result.verified);
815
+ assertEquals(result.reason.type, "keyFetchError");
816
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(count));
817
+ assertEquals(documentLoader.loaded, [
818
+ 1,
819
+ 2,
820
+ 3,
821
+ 4,
822
+ 5
823
+ ].slice(0, count).map(missing));
824
+ }
825
+ });
826
+ await t.step("accepts a valid signature within the cap", async () => {
827
+ const documentLoader = createCountingKeyLoader();
828
+ const result = await verifyRequestDetailed(await signRfc9421Many([
829
+ missing(1),
830
+ missing(2),
831
+ valid,
832
+ missing(4)
833
+ ], currentTime), {
834
+ documentLoader,
835
+ contextLoader: mockDocumentLoader,
836
+ currentTime
837
+ });
838
+ assert(result.verified);
839
+ assertEquals(result.key, rsaPublicKey2);
840
+ assertEquals(result.signatureLabel, "sig3");
841
+ assertEquals(documentLoader.loaded, [
842
+ missing(1),
843
+ missing(2),
844
+ valid
845
+ ]);
846
+ });
847
+ await t.step("ignores a valid signature beyond the cap", async () => {
848
+ const request = await signRfc9421Many([
849
+ missing(1),
850
+ missing(2),
851
+ missing(3),
852
+ valid
853
+ ], currentTime);
854
+ const documentLoader = createCountingKeyLoader();
855
+ const result = await verifyRequestDetailed(request, {
856
+ documentLoader,
857
+ contextLoader: mockDocumentLoader,
858
+ currentTime
859
+ });
860
+ assertFalse(result.verified);
861
+ assertEquals(result.reason.type, "keyFetchError");
862
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(3));
863
+ assertEquals(documentLoader.loaded, [
864
+ 1,
865
+ 2,
866
+ 3
867
+ ].map(missing));
868
+ assertEquals(await verifyRequest(request, {
869
+ documentLoader: createCountingKeyLoader(),
870
+ contextLoader: mockDocumentLoader,
871
+ currentTime,
872
+ maxSignatures: 4
873
+ }), rsaPublicKey2);
874
+ });
875
+ await t.step("tries signatures in the header order", async () => {
876
+ const request = await signRfc9421Many([
877
+ {
878
+ label: "z",
879
+ keyId: missing(1)
880
+ },
881
+ {
882
+ label: "a",
883
+ keyId: valid
884
+ },
885
+ {
886
+ label: "m",
887
+ keyId: missing(3)
888
+ }
889
+ ], currentTime);
890
+ const documentLoader = createCountingKeyLoader();
891
+ assertEquals(await verifyRequest(request, {
892
+ documentLoader,
893
+ contextLoader: mockDocumentLoader,
894
+ currentTime,
895
+ maxSignatures: 1
896
+ }), null);
897
+ assertEquals(documentLoader.loaded, [missing(1)]);
898
+ });
899
+ await t.step("counts signatures that fail before their keys are fetched", async () => {
900
+ const outdated = await signRfc9421Many([{
901
+ label: "old",
902
+ keyId: missing(1)
903
+ }], currentTime.subtract({ hours: 2 }));
904
+ const signed = await signRfc9421Many([
905
+ {
906
+ label: "nobytes1",
907
+ keyId: missing(2)
908
+ },
909
+ {
910
+ label: "nobytes2",
911
+ keyId: missing(3)
912
+ },
913
+ {
914
+ label: "valid",
915
+ keyId: valid
916
+ }
917
+ ], currentTime);
918
+ const inputs = {
919
+ ...splitRfc9421Header(outdated.headers.get("Signature-Input")),
920
+ ...splitRfc9421Header(signed.headers.get("Signature-Input"))
921
+ };
922
+ const signatures = {
923
+ ...splitRfc9421Header(outdated.headers.get("Signature")),
924
+ ...splitRfc9421Header(signed.headers.get("Signature"))
925
+ };
926
+ const build = (labels) => {
927
+ const headers = new Headers(signed.headers);
928
+ headers.set("Signature-Input", [`dropped=("@method");keyid="${missing(0)}"`, ...labels.map((label) => inputs[label])].join(", "));
929
+ headers.set("Signature", labels.filter((label) => !label.startsWith("nobytes")).map((label) => signatures[label]).join(", "));
930
+ return new Request(signed.url, {
931
+ method: "POST",
932
+ headers,
933
+ body: "Hello, world!"
934
+ });
935
+ };
936
+ let documentLoader = createCountingKeyLoader();
937
+ let result = await verifyRequestDetailed(build([
938
+ "old",
939
+ "nobytes1",
940
+ "valid"
941
+ ]), {
942
+ documentLoader,
943
+ contextLoader: mockDocumentLoader,
944
+ currentTime
945
+ });
946
+ assert(result.verified);
947
+ assertEquals(result.signatureLabel, "valid");
948
+ assertEquals(documentLoader.loaded, [valid]);
949
+ documentLoader = createCountingKeyLoader();
950
+ result = await verifyRequestDetailed(build([
951
+ "old",
952
+ "nobytes1",
953
+ "nobytes2",
954
+ "valid"
955
+ ]), {
956
+ documentLoader,
957
+ contextLoader: mockDocumentLoader,
958
+ currentTime
959
+ });
960
+ assertFalse(result.verified);
961
+ assertEquals(documentLoader.loaded, []);
962
+ });
963
+ await t.step("rejects an invalid maxSignatures", async () => {
964
+ const unsigned = new Request("https://example.com/inbox");
965
+ for (const maxSignatures of [
966
+ 0,
967
+ -1,
968
+ 1.5,
969
+ NaN,
970
+ 2 ** 53,
971
+ -Infinity
972
+ ]) {
973
+ await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures }), RangeError);
974
+ await assertRejects(() => verifyRequest(unsigned, { maxSignatures }), RangeError);
975
+ }
976
+ await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures: null }), RangeError);
977
+ });
978
+ });
979
+ test("verifyRequestDetailed() [rfc9421] looks up each key once per request", async (t) => {
980
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
981
+ const missing = (n) => `https://example.com/missing-${n}`;
982
+ const valid = "https://example.com/key2";
983
+ const forged = {
984
+ keyId: valid,
985
+ privateKey: rsaPrivateKey3
986
+ };
987
+ await t.step("signatures naming the same key", async () => {
988
+ const request = await signRfc9421Many([
989
+ forged,
990
+ forged,
991
+ valid
992
+ ], currentTime);
993
+ for (const keyCache of [void 0, new KvKeyCache(new MemoryKvStore(), ["pk"])]) {
994
+ const documentLoader = createCountingKeyLoader();
995
+ const result = await verifyRequestDetailed(request, {
996
+ documentLoader,
997
+ contextLoader: mockDocumentLoader,
998
+ currentTime,
999
+ keyCache
1000
+ });
1001
+ assert(result.verified);
1002
+ assertEquals(result.signatureLabel, "sig3");
1003
+ assertEquals(documentLoader.loaded, [valid]);
1004
+ }
1005
+ });
1006
+ await t.step("signatures naming the same missing key", async () => {
1007
+ const documentLoader = createCountingKeyLoader();
1008
+ const result = await verifyRequestDetailed(await signRfc9421Many([
1009
+ missing(1),
1010
+ missing(1),
1011
+ missing(1)
1012
+ ], currentTime), {
1013
+ documentLoader,
1014
+ contextLoader: mockDocumentLoader,
1015
+ currentTime
1016
+ });
1017
+ assertFalse(result.verified);
1018
+ assertEquals(result.reason.type, "keyFetchError");
1019
+ assertEquals(documentLoader.loaded, [missing(1)]);
1020
+ });
1021
+ await t.step("separate requests", async () => {
1022
+ const request = await signRfc9421Many([valid], currentTime);
1023
+ const documentLoader = createCountingKeyLoader();
1024
+ const options = {
1025
+ documentLoader,
1026
+ contextLoader: mockDocumentLoader,
1027
+ currentTime
1028
+ };
1029
+ assertEquals(await verifyRequest(request, options), rsaPublicKey2);
1030
+ assertEquals(await verifyRequest(request, options), rsaPublicKey2);
1031
+ assertEquals(documentLoader.loaded, [valid, valid]);
1032
+ });
1033
+ const createStaleKeyCache = () => {
1034
+ const gets = [];
1035
+ return {
1036
+ keyCache: {
1037
+ get(keyId) {
1038
+ gets.push(keyId.href);
1039
+ return Promise.resolve(keyId.href === valid ? rsaPublicKey1 : keyId.href === missing(1) ? null : void 0);
1040
+ },
1041
+ set: () => Promise.resolve(),
1042
+ getFetchError: (keyId) => Promise.resolve(keyId.href === missing(1) ? { error: new FetchError(keyId, "Cached failure") } : void 0),
1043
+ setFetchError: () => Promise.resolve()
1044
+ },
1045
+ gets
1046
+ };
1047
+ };
1048
+ await t.step("refreshes a stale cached key once, only for its signature", async () => {
1049
+ const { keyCache, gets } = createStaleKeyCache();
1050
+ const documentLoader = createCountingKeyLoader();
1051
+ const result = await verifyRequestDetailed(await signRfc9421Many([
1052
+ missing(1),
1053
+ missing(2),
1054
+ forged,
1055
+ valid
1056
+ ], currentTime), {
1057
+ documentLoader,
1058
+ contextLoader: mockDocumentLoader,
1059
+ currentTime,
1060
+ keyCache,
1061
+ maxSignatures: 4
1062
+ });
1063
+ assert(result.verified);
1064
+ assertEquals(result.key, rsaPublicKey2);
1065
+ assertEquals(result.signatureLabel, "sig4");
1066
+ assertEquals(documentLoader.loaded, [missing(2), valid]);
1067
+ assertEquals(gets, [
1068
+ missing(1),
1069
+ missing(2),
1070
+ valid
1071
+ ]);
1072
+ });
1073
+ await t.step("refreshes each stale cached key at most once", async () => {
1074
+ const stale = (n) => `https://example.com/stale-${n}`;
1075
+ const gets = [];
1076
+ const keyCache = {
1077
+ get(keyId) {
1078
+ gets.push(keyId.href);
1079
+ return Promise.resolve(rsaPublicKey1);
1080
+ },
1081
+ set: () => Promise.resolve()
1082
+ };
1083
+ const documentLoader = createCountingKeyLoader();
1084
+ const result = await verifyRequestDetailed(await signRfc9421Many([
1085
+ stale(1),
1086
+ stale(2),
1087
+ stale(1),
1088
+ stale(4)
1089
+ ], currentTime), {
1090
+ documentLoader,
1091
+ contextLoader: mockDocumentLoader,
1092
+ currentTime,
1093
+ keyCache
1094
+ });
1095
+ assertFalse(result.verified);
1096
+ assertEquals(result.reason.type, "keyFetchError");
1097
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, stale(1));
1098
+ assertEquals(documentLoader.loaded, [stale(1), stale(2)]);
1099
+ assertEquals(gets, [stale(1), stale(2)]);
1100
+ });
1101
+ await t.step("does not refresh a cached key for other failures", async () => {
1102
+ const [input] = Object.values(splitRfc9421Header((await signRfc9421Many([valid], currentTime)).headers.get("Signature-Input")));
1103
+ const withAlg = (alg) => /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, `;alg="${alg}"`) : `${input};alg="${alg}"`;
1104
+ for (const signatureInput of [withAlg("unknown"), input.replace(/^sig1=\([^)]*\)/, "sig1=(\"@unsupported\")")]) {
1105
+ const request = await signRfc9421Many([valid], currentTime);
1106
+ request.headers.set("Signature-Input", signatureInput);
1107
+ const { keyCache } = createStaleKeyCache();
1108
+ const documentLoader = createCountingKeyLoader();
1109
+ assertEquals(await verifyRequest(request, {
1110
+ documentLoader,
1111
+ contextLoader: mockDocumentLoader,
1112
+ currentTime,
1113
+ keyCache
1114
+ }), null, signatureInput);
1115
+ assertEquals(documentLoader.loaded, [], signatureInput);
1116
+ }
1117
+ });
1118
+ await t.step("refreshes a cached key of another algorithm family", async () => {
1119
+ const keyId = ed25519PublicKey.id.href;
1120
+ const request = await signRfc9421Many([keyId], currentTime);
1121
+ const input = request.headers.get("Signature-Input");
1122
+ request.headers.set("Signature-Input", /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, ";alg=\"ed25519\"") : `${input};alg="ed25519"`);
1123
+ const { sig1 } = parseRfc9421SignatureInput(request.headers.get("Signature-Input"));
1124
+ const signature = await crypto.subtle.sign("Ed25519", ed25519PrivateKey, new TextEncoder().encode(createRfc9421SignatureBase(request, sig1.components, sig1.parameters)));
1125
+ request.headers.set("Signature", `sig1=:${encodeBase64(new Uint8Array(signature))}:`);
1126
+ const loaded = [];
1127
+ const result = await verifyRequestDetailed(request, {
1128
+ documentLoader: (url, options) => {
1129
+ loaded.push(url);
1130
+ return mockDocumentLoader(url, options);
1131
+ },
1132
+ contextLoader: mockDocumentLoader,
1133
+ currentTime,
1134
+ keyCache: {
1135
+ get: (id) => Promise.resolve(id.href === keyId ? rsaPublicKey1 : void 0),
1136
+ set: () => Promise.resolve()
1137
+ }
1138
+ });
1139
+ assert(result.verified);
1140
+ assertEquals(result.key.id?.href, keyId);
1141
+ assertEquals(loaded.length, 1);
1142
+ });
1143
+ });
631
1144
  test("verifyRequest() [rfc9421] manual POST verification", async () => {
632
1145
  const currentTimestamp = 1709626184;
633
1146
  const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
@@ -737,7 +1250,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
737
1250
  contextLoader: mockDocumentLoader,
738
1251
  documentLoader: mockDocumentLoader,
739
1252
  spec: "rfc9421",
740
- currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 2592e3) * 1e3)).toISOString()}`),
1253
+ currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1712218184 * 1e3)).toISOString()}`),
741
1254
  timeWindow: { hours: 1 }
742
1255
  }), null, "Should fail verification when signature timestamp is too old");
743
1256
  assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
@@ -769,7 +1282,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
769
1282
  contextLoader: mockDocumentLoader,
770
1283
  documentLoader: mockDocumentLoader,
771
1284
  spec: "rfc9421",
772
- currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 31536e3) * 1e3)).toISOString()}`),
1285
+ currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1741162184 * 1e3)).toISOString()}`),
773
1286
  timeWindow: false
774
1287
  }), rsaPublicKey2, "Should verify signature when time checking is disabled");
775
1288
  const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
@@ -1001,6 +1514,7 @@ test("doubleKnock() function with redirect handling", async () => {
1001
1514
  esm_default.spyGlobal();
1002
1515
  const requestedUrls = [];
1003
1516
  const responseCodes = [];
1517
+ const validatedRedirects = [];
1004
1518
  esm_default.post("https://example.com/redirect-endpoint", (cl) => {
1005
1519
  requestedUrls.push(cl.url);
1006
1520
  responseCodes.push(302);
@@ -1018,11 +1532,41 @@ test("doubleKnock() function with redirect handling", async () => {
1018
1532
  }), {
1019
1533
  keyId: rsaPublicKey2.id,
1020
1534
  privateKey: rsaPrivateKey2
1021
- })).status, 202, "Final response status should be 202 Accepted");
1535
+ }, { validateRedirect(url) {
1536
+ validatedRedirects.push(url);
1537
+ } })).status, 202, "Final response status should be 202 Accepted");
1022
1538
  assertEquals(requestedUrls.length, 2, "Two URLs should have been requested");
1023
1539
  assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint");
1024
1540
  assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint");
1025
1541
  assertEquals(responseCodes, [302, 202], "Response status codes should match expected sequence");
1542
+ assertEquals(validatedRedirects, ["https://example.com/final-endpoint"]);
1543
+ esm_default.hardReset();
1544
+ });
1545
+ test("doubleKnock() validates redirects after signature fallback", async () => {
1546
+ esm_default.spyGlobal();
1547
+ let requestCount = 0;
1548
+ let privateRequestCount = 0;
1549
+ esm_default.post("https://example.com/redirect-after-fallback", (cl) => {
1550
+ requestCount++;
1551
+ if (cl.request.headers.has("Signature-Input")) return new Response("Unauthorized", { status: 401 });
1552
+ return Response.redirect("http://localhost/private", 302);
1553
+ });
1554
+ esm_default.post("http://localhost/private", () => {
1555
+ privateRequestCount++;
1556
+ return new Response("", { status: 202 });
1557
+ });
1558
+ const request = new Request("https://example.com/redirect-after-fallback", {
1559
+ method: "POST",
1560
+ body: "Test message"
1561
+ });
1562
+ await assertRejects(() => doubleKnock(request, {
1563
+ keyId: rsaPublicKey2.id,
1564
+ privateKey: rsaPrivateKey2
1565
+ }, { validateRedirect(url) {
1566
+ throw new Error(`Disallowed redirect: ${url}`);
1567
+ } }), Error, "Disallowed redirect: http://localhost/private");
1568
+ assertEquals(requestCount, 2);
1569
+ assertEquals(privateRequestCount, 0);
1026
1570
  esm_default.hardReset();
1027
1571
  });
1028
1572
  test("doubleKnock() function with both specs rejected", async () => {
@@ -1813,6 +2357,37 @@ test("doubleKnock(): Accept-Signature challenge retry succeeds", async () => {
1813
2357
  assertEquals(requestCount, 2);
1814
2358
  esm_default.hardReset();
1815
2359
  });
2360
+ test("doubleKnock(): validates redirects after Accept-Signature challenge retry", async () => {
2361
+ esm_default.spyGlobal();
2362
+ let requestCount = 0;
2363
+ let privateRequestCount = 0;
2364
+ esm_default.post("https://example.com/inbox-challenge-redirect", () => {
2365
+ requestCount++;
2366
+ if (requestCount === 1) return new Response("Not Authorized", {
2367
+ status: 401,
2368
+ headers: { "Accept-Signature": "sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\");created;nonce=\"challenge-nonce-redirect\"" }
2369
+ });
2370
+ return Response.redirect("http://localhost/private", 302);
2371
+ });
2372
+ esm_default.post("http://localhost/private", () => {
2373
+ privateRequestCount++;
2374
+ return new Response("", { status: 202 });
2375
+ });
2376
+ const request = new Request("https://example.com/inbox-challenge-redirect", {
2377
+ method: "POST",
2378
+ body: "Test message",
2379
+ headers: { "Content-Type": "text/plain" }
2380
+ });
2381
+ await assertRejects(() => doubleKnock(request, {
2382
+ keyId: rsaPublicKey2.id,
2383
+ privateKey: rsaPrivateKey2
2384
+ }, { validateRedirect(url) {
2385
+ throw new Error(`Disallowed redirect: ${url}`);
2386
+ } }), Error, "Disallowed redirect: http://localhost/private");
2387
+ assertEquals(requestCount, 2);
2388
+ assertEquals(privateRequestCount, 0);
2389
+ esm_default.hardReset();
2390
+ });
1816
2391
  test("doubleKnock(): unfulfillable Accept-Signature falls to legacy fallback", async () => {
1817
2392
  esm_default.spyGlobal();
1818
2393
  let requestCount = 0;
@@ -2009,5 +2584,158 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
2009
2584
  assertEquals(requestCount, 2);
2010
2585
  esm_default.hardReset();
2011
2586
  });
2587
+ test("listRequestSignatures()", async (t) => {
2588
+ const request = () => new Request("https://example.com/inbox", {
2589
+ method: "POST",
2590
+ body: "Test message"
2591
+ });
2592
+ await t.step("draft-cavage", async () => {
2593
+ const signed = await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id);
2594
+ const signatures = listRequestSignatures(signed);
2595
+ assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, null]]);
2596
+ const components = signatures[0].components;
2597
+ assertEquals(components.map((c) => c.value), signed.headers.get("Signature").match(/headers="([^"]*)"/)[1].split(" "));
2598
+ assert(components.every((c) => Object.keys(c.params).length < 1));
2599
+ });
2600
+ await t.step("RFC 9421", async () => {
2601
+ const signatures = listRequestSignatures(await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id, {
2602
+ spec: "rfc9421",
2603
+ rfc9421: { label: "a" }
2604
+ }));
2605
+ assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, "a"]]);
2606
+ assertEquals(signatures[0].components.map((c) => c.value), [
2607
+ "@method",
2608
+ "@target-uri",
2609
+ "@authority",
2610
+ "host",
2611
+ "date",
2612
+ "content-digest"
2613
+ ]);
2614
+ });
2615
+ await t.step("RFC 9421 beyond the limit", async () => {
2616
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
2617
+ const signed = await signRfc9421Many([
2618
+ 1,
2619
+ 2,
2620
+ 3,
2621
+ 4
2622
+ ].map((n) => `https://example.com/key-${n}`), currentTime);
2623
+ const headers = new Headers(signed.headers);
2624
+ headers.set("Signature-Input", headers.get("Signature-Input").replace("https://example.com/key-1", "not a URL"));
2625
+ const request = new Request(signed.url, {
2626
+ method: "POST",
2627
+ headers
2628
+ });
2629
+ const keyIds = (maxSignatures) => listRequestSignatures(request, maxSignatures).map((s) => s.keyId.href);
2630
+ assertEquals(keyIds(), ["https://example.com/key-2", "https://example.com/key-3"]);
2631
+ assertEquals(keyIds(1), []);
2632
+ assertEquals(keyIds(Infinity), [
2633
+ "https://example.com/key-2",
2634
+ "https://example.com/key-3",
2635
+ "https://example.com/key-4"
2636
+ ]);
2637
+ });
2638
+ await t.step("unsigned or malformed", () => {
2639
+ assertEquals(listRequestSignatures(request()), []);
2640
+ const malformed = request();
2641
+ malformed.headers.set("Signature", "keyId=\"not a URL\",signature=\"AA==\"");
2642
+ assertEquals(listRequestSignatures(malformed), []);
2643
+ const garbage = request();
2644
+ garbage.headers.set("Signature-Input", "((");
2645
+ garbage.headers.set("Signature", "((");
2646
+ assertEquals(listRequestSignatures(garbage), []);
2647
+ });
2648
+ });
2649
+ test("selectRequestSignature()", async () => {
2650
+ const body = "Test message";
2651
+ const sign = (label, keyId) => signRequest(new Request("https://example.com/inbox", {
2652
+ method: "POST",
2653
+ body
2654
+ }), rsaPrivateKey2, keyId, {
2655
+ spec: "rfc9421",
2656
+ rfc9421: { label }
2657
+ });
2658
+ const first = await sign("a", new URL("https://example.com/other#key"));
2659
+ const second = await sign("b", rsaPublicKey2.id);
2660
+ const headers = new Headers(second.headers);
2661
+ for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${first.headers.get(name)}, ${second.headers.get(name)}`);
2662
+ const request = new Request(second.url, {
2663
+ method: "POST",
2664
+ headers,
2665
+ body
2666
+ });
2667
+ const signatures = listRequestSignatures(request);
2668
+ assertEquals(signatures.map((s) => s.label), ["a", "b"]);
2669
+ const selected = selectRequestSignature(request, signatures[1]);
2670
+ assertEquals(listRequestSignatures(selected).map((s) => [s.label, s.keyId.href]), [["b", rsaPublicKey2.id.href]]);
2671
+ const result = await verifyRequestDetailed(selected, {
2672
+ documentLoader: mockDocumentLoader,
2673
+ contextLoader: mockDocumentLoader
2674
+ });
2675
+ assert(result.verified);
2676
+ assertEquals(result.key.id, rsaPublicKey2.id);
2677
+ assertEquals(await selected.text(), body);
2678
+ assertEquals(await request.text(), body);
2679
+ });
2680
+ test("verifyRequestDetailed() reports a timed-out key fetch", async (t) => {
2681
+ for (const stall of ["headers", "body"]) await t.step(`stalled ${stall}`, async () => {
2682
+ esm_default.mockGlobal();
2683
+ const keyId = new URL(`https://slow.example/actors/alice#${stall}`);
2684
+ let requests = 0;
2685
+ esm_default.get(`begin:https://slow.example/actors/alice`, () => {
2686
+ requests++;
2687
+ if (stall === "headers") return new Promise(() => {});
2688
+ return new Response(new ReadableStream({ pull: () => new Promise(() => {}) }), { status: 404 });
2689
+ });
2690
+ const loader = getDocumentLoader({
2691
+ allowPrivateAddress: true,
2692
+ timeout: 100
2693
+ });
2694
+ const documentLoader = (url, options) => url.startsWith("https://slow.example/") ? loader(url, options) : mockDocumentLoader(url);
2695
+ const kv = new MemoryKvStore();
2696
+ try {
2697
+ const sign = () => signRequest(new Request("https://example.com/inbox", {
2698
+ method: "POST",
2699
+ headers: {
2700
+ "Content-Type": "application/activity+json",
2701
+ accept: "application/ld+json"
2702
+ },
2703
+ body: JSON.stringify({
2704
+ "@context": "https://www.w3.org/ns/activitystreams",
2705
+ type: "Create",
2706
+ actor: "https://slow.example/actors/alice"
2707
+ })
2708
+ }), rsaPrivateKey2, keyId);
2709
+ const result = await verifyRequestDetailed(await sign(), {
2710
+ contextLoader: mockDocumentLoader,
2711
+ documentLoader,
2712
+ keyCache: new KvKeyCache(kv, ["pk"])
2713
+ });
2714
+ assertFalse(result.verified);
2715
+ assert(result.reason.type === "keyFetchError");
2716
+ assertEquals(result.reason.keyId, keyId);
2717
+ assert("error" in result.reason.result);
2718
+ const error = result.reason.result.error;
2719
+ assert(error instanceof FetchError);
2720
+ assertEquals(error.response, void 0);
2721
+ assert(error.cause instanceof DOMException);
2722
+ assertEquals(error.cause.name, "TimeoutError");
2723
+ assertEquals(requests, 1);
2724
+ const cached = await verifyRequestDetailed(await sign(), {
2725
+ contextLoader: mockDocumentLoader,
2726
+ documentLoader,
2727
+ keyCache: new KvKeyCache(kv, ["pk"])
2728
+ });
2729
+ assertFalse(cached.verified);
2730
+ assert(cached.reason.type === "keyFetchError");
2731
+ assert("error" in cached.reason.result);
2732
+ assertEquals(cached.reason.result.error.name, "FetchError");
2733
+ assertStringIncludes(cached.reason.result.error.message, "Timed out after 100 ms");
2734
+ assertEquals(requests, 1);
2735
+ } finally {
2736
+ esm_default.hardReset();
2737
+ }
2738
+ });
2739
+ });
2012
2740
  //#endregion
2013
2741
  export {};