@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,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-BspPiXqn.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-siciWkmx.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-Da8A8mZa.mjs";
19
+ import { FetchError, 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();
@@ -628,6 +660,430 @@ test("verifyRequest() [rfc9421] successful GET verification", async () => {
628
660
  currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(currentTimestamp * 1e3)).toISOString()}`)
629
661
  }), rsaPublicKey2, "Valid signature should verify to the correct public key");
630
662
  });
663
+ for (const failure of [
664
+ "missing key",
665
+ "invalid signature",
666
+ "digest mismatch",
667
+ "empty body",
668
+ "cached key"
669
+ ]) test(`verifyRequest() [rfc9421] multiple POST signatures: ${failure}`, async () => {
670
+ const body = failure === "empty body" ? "" : "Hello, world!";
671
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
672
+ const signed = await signRequest(new Request("https://example.com/inbox", {
673
+ method: "POST",
674
+ body
675
+ }), rsaPrivateKey2, new URL("https://example.com/key2"), {
676
+ spec: "rfc9421",
677
+ currentTime
678
+ });
679
+ const input = signed.headers.get("Signature-Input");
680
+ const signature = signed.headers.get("Signature");
681
+ const firstInput = failure === "missing key" ? input.replace("https://example.com/key2", "https://example.com/missing-key") : input;
682
+ signed.headers.set("Signature-Input", `${firstInput}, ${input.replace(/^sig1=/, "sig2=")}`);
683
+ signed.headers.set("Signature", `sig1=:AAAAAA==:, ${signature.replace(/^sig1=/, "sig2=")}`);
684
+ const request = failure === "digest mismatch" ? new Request(signed, { body: "Tampered body" }) : signed;
685
+ const loaded = [];
686
+ assertEquals(await verifyRequest(request, {
687
+ keyCache: failure === "cached key" ? {
688
+ get: () => Promise.resolve(rsaPublicKey1),
689
+ set: async () => {}
690
+ } : void 0,
691
+ spec: "rfc9421",
692
+ currentTime,
693
+ contextLoader: mockDocumentLoader,
694
+ documentLoader: (url, options) => {
695
+ loaded.push(url);
696
+ return mockDocumentLoader(url, options);
697
+ }
698
+ }), failure === "digest mismatch" ? null : rsaPublicKey2);
699
+ assertEquals(loaded, failure === "digest mismatch" ? [] : failure === "missing key" ? ["https://example.com/missing-key", "https://example.com/key2"] : ["https://example.com/key2"]);
700
+ assertFalse(request.bodyUsed);
701
+ assertEquals(await request.text(), failure === "digest mismatch" ? "Tampered body" : body);
702
+ });
703
+ async function signRfc9421Many(signatures, currentTime) {
704
+ let request = new Request("https://example.com/inbox", {
705
+ method: "POST",
706
+ body: "Hello, world!"
707
+ });
708
+ let i = 0;
709
+ for (const signature of signatures) {
710
+ const { label, keyId, privateKey } = typeof signature === "string" ? { keyId: signature } : signature;
711
+ i++;
712
+ request = await signRequest(request, privateKey ?? rsaPrivateKey2, new URL(keyId), {
713
+ spec: "rfc9421",
714
+ currentTime,
715
+ rfc9421: { label: label ?? `sig${i}` }
716
+ });
717
+ }
718
+ return request;
719
+ }
720
+ function splitRfc9421Header(header) {
721
+ return Object.fromEntries(header.split(/,\s*/).map((member) => [member.slice(0, member.indexOf("=")), member]));
722
+ }
723
+ function createCountingKeyLoader() {
724
+ const loaded = [];
725
+ const loader = (url, options) => {
726
+ loaded.push(url);
727
+ if (url === "https://example.com/key2") return mockDocumentLoader(url, options);
728
+ return Promise.reject(new FetchError(url, `Not found: ${url}`));
729
+ };
730
+ return Object.assign(loader, { loaded });
731
+ }
732
+ test("verifyRequestDetailed() [rfc9421] caps the number of signatures", async (t) => {
733
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
734
+ const missing = (n) => `https://example.com/missing-${n}`;
735
+ const valid = "https://example.com/key2";
736
+ await t.step("fetches only the keys of the first signatures", async () => {
737
+ const request = await signRfc9421Many([
738
+ 1,
739
+ 2,
740
+ 3,
741
+ 4,
742
+ 5
743
+ ].map(missing), currentTime);
744
+ for (const [maxSignatures, count] of [
745
+ [void 0, 3],
746
+ [1, 1],
747
+ [5, 5],
748
+ [Infinity, 5]
749
+ ]) {
750
+ const documentLoader = createCountingKeyLoader();
751
+ const result = await verifyRequestDetailed(request, {
752
+ documentLoader,
753
+ contextLoader: mockDocumentLoader,
754
+ currentTime,
755
+ maxSignatures
756
+ });
757
+ assertFalse(result.verified);
758
+ assertEquals(result.reason.type, "keyFetchError");
759
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(count));
760
+ assertEquals(documentLoader.loaded, [
761
+ 1,
762
+ 2,
763
+ 3,
764
+ 4,
765
+ 5
766
+ ].slice(0, count).map(missing));
767
+ }
768
+ });
769
+ await t.step("accepts a valid signature within the cap", async () => {
770
+ const documentLoader = createCountingKeyLoader();
771
+ const result = await verifyRequestDetailed(await signRfc9421Many([
772
+ missing(1),
773
+ missing(2),
774
+ valid,
775
+ missing(4)
776
+ ], currentTime), {
777
+ documentLoader,
778
+ contextLoader: mockDocumentLoader,
779
+ currentTime
780
+ });
781
+ assert(result.verified);
782
+ assertEquals(result.key, rsaPublicKey2);
783
+ assertEquals(result.signatureLabel, "sig3");
784
+ assertEquals(documentLoader.loaded, [
785
+ missing(1),
786
+ missing(2),
787
+ valid
788
+ ]);
789
+ });
790
+ await t.step("ignores a valid signature beyond the cap", async () => {
791
+ const request = await signRfc9421Many([
792
+ missing(1),
793
+ missing(2),
794
+ missing(3),
795
+ valid
796
+ ], currentTime);
797
+ const documentLoader = createCountingKeyLoader();
798
+ const result = await verifyRequestDetailed(request, {
799
+ documentLoader,
800
+ contextLoader: mockDocumentLoader,
801
+ currentTime
802
+ });
803
+ assertFalse(result.verified);
804
+ assertEquals(result.reason.type, "keyFetchError");
805
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, missing(3));
806
+ assertEquals(documentLoader.loaded, [
807
+ 1,
808
+ 2,
809
+ 3
810
+ ].map(missing));
811
+ assertEquals(await verifyRequest(request, {
812
+ documentLoader: createCountingKeyLoader(),
813
+ contextLoader: mockDocumentLoader,
814
+ currentTime,
815
+ maxSignatures: 4
816
+ }), rsaPublicKey2);
817
+ });
818
+ await t.step("tries signatures in the header order", async () => {
819
+ const request = await signRfc9421Many([
820
+ {
821
+ label: "z",
822
+ keyId: missing(1)
823
+ },
824
+ {
825
+ label: "a",
826
+ keyId: valid
827
+ },
828
+ {
829
+ label: "m",
830
+ keyId: missing(3)
831
+ }
832
+ ], currentTime);
833
+ const documentLoader = createCountingKeyLoader();
834
+ assertEquals(await verifyRequest(request, {
835
+ documentLoader,
836
+ contextLoader: mockDocumentLoader,
837
+ currentTime,
838
+ maxSignatures: 1
839
+ }), null);
840
+ assertEquals(documentLoader.loaded, [missing(1)]);
841
+ });
842
+ await t.step("counts signatures that fail before their keys are fetched", async () => {
843
+ const outdated = await signRfc9421Many([{
844
+ label: "old",
845
+ keyId: missing(1)
846
+ }], currentTime.subtract({ hours: 2 }));
847
+ const signed = await signRfc9421Many([
848
+ {
849
+ label: "nobytes1",
850
+ keyId: missing(2)
851
+ },
852
+ {
853
+ label: "nobytes2",
854
+ keyId: missing(3)
855
+ },
856
+ {
857
+ label: "valid",
858
+ keyId: valid
859
+ }
860
+ ], currentTime);
861
+ const inputs = {
862
+ ...splitRfc9421Header(outdated.headers.get("Signature-Input")),
863
+ ...splitRfc9421Header(signed.headers.get("Signature-Input"))
864
+ };
865
+ const signatures = {
866
+ ...splitRfc9421Header(outdated.headers.get("Signature")),
867
+ ...splitRfc9421Header(signed.headers.get("Signature"))
868
+ };
869
+ const build = (labels) => {
870
+ const headers = new Headers(signed.headers);
871
+ headers.set("Signature-Input", [`dropped=("@method");keyid="${missing(0)}"`, ...labels.map((label) => inputs[label])].join(", "));
872
+ headers.set("Signature", labels.filter((label) => !label.startsWith("nobytes")).map((label) => signatures[label]).join(", "));
873
+ return new Request(signed.url, {
874
+ method: "POST",
875
+ headers,
876
+ body: "Hello, world!"
877
+ });
878
+ };
879
+ let documentLoader = createCountingKeyLoader();
880
+ let result = await verifyRequestDetailed(build([
881
+ "old",
882
+ "nobytes1",
883
+ "valid"
884
+ ]), {
885
+ documentLoader,
886
+ contextLoader: mockDocumentLoader,
887
+ currentTime
888
+ });
889
+ assert(result.verified);
890
+ assertEquals(result.signatureLabel, "valid");
891
+ assertEquals(documentLoader.loaded, [valid]);
892
+ documentLoader = createCountingKeyLoader();
893
+ result = await verifyRequestDetailed(build([
894
+ "old",
895
+ "nobytes1",
896
+ "nobytes2",
897
+ "valid"
898
+ ]), {
899
+ documentLoader,
900
+ contextLoader: mockDocumentLoader,
901
+ currentTime
902
+ });
903
+ assertFalse(result.verified);
904
+ assertEquals(documentLoader.loaded, []);
905
+ });
906
+ await t.step("rejects an invalid maxSignatures", async () => {
907
+ const unsigned = new Request("https://example.com/inbox");
908
+ for (const maxSignatures of [
909
+ 0,
910
+ -1,
911
+ 1.5,
912
+ NaN,
913
+ 2 ** 53,
914
+ -Infinity
915
+ ]) {
916
+ await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures }), RangeError);
917
+ await assertRejects(() => verifyRequest(unsigned, { maxSignatures }), RangeError);
918
+ }
919
+ await assertRejects(() => verifyRequestDetailed(unsigned, { maxSignatures: null }), RangeError);
920
+ });
921
+ });
922
+ test("verifyRequestDetailed() [rfc9421] looks up each key once per request", async (t) => {
923
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
924
+ const missing = (n) => `https://example.com/missing-${n}`;
925
+ const valid = "https://example.com/key2";
926
+ const forged = {
927
+ keyId: valid,
928
+ privateKey: rsaPrivateKey3
929
+ };
930
+ await t.step("signatures naming the same key", async () => {
931
+ const request = await signRfc9421Many([
932
+ forged,
933
+ forged,
934
+ valid
935
+ ], currentTime);
936
+ for (const keyCache of [void 0, new KvKeyCache(new MemoryKvStore(), ["pk"])]) {
937
+ const documentLoader = createCountingKeyLoader();
938
+ const result = await verifyRequestDetailed(request, {
939
+ documentLoader,
940
+ contextLoader: mockDocumentLoader,
941
+ currentTime,
942
+ keyCache
943
+ });
944
+ assert(result.verified);
945
+ assertEquals(result.signatureLabel, "sig3");
946
+ assertEquals(documentLoader.loaded, [valid]);
947
+ }
948
+ });
949
+ await t.step("signatures naming the same missing key", async () => {
950
+ const documentLoader = createCountingKeyLoader();
951
+ const result = await verifyRequestDetailed(await signRfc9421Many([
952
+ missing(1),
953
+ missing(1),
954
+ missing(1)
955
+ ], currentTime), {
956
+ documentLoader,
957
+ contextLoader: mockDocumentLoader,
958
+ currentTime
959
+ });
960
+ assertFalse(result.verified);
961
+ assertEquals(result.reason.type, "keyFetchError");
962
+ assertEquals(documentLoader.loaded, [missing(1)]);
963
+ });
964
+ await t.step("separate requests", async () => {
965
+ const request = await signRfc9421Many([valid], currentTime);
966
+ const documentLoader = createCountingKeyLoader();
967
+ const options = {
968
+ documentLoader,
969
+ contextLoader: mockDocumentLoader,
970
+ currentTime
971
+ };
972
+ assertEquals(await verifyRequest(request, options), rsaPublicKey2);
973
+ assertEquals(await verifyRequest(request, options), rsaPublicKey2);
974
+ assertEquals(documentLoader.loaded, [valid, valid]);
975
+ });
976
+ const createStaleKeyCache = () => {
977
+ const gets = [];
978
+ return {
979
+ keyCache: {
980
+ get(keyId) {
981
+ gets.push(keyId.href);
982
+ return Promise.resolve(keyId.href === valid ? rsaPublicKey1 : keyId.href === missing(1) ? null : void 0);
983
+ },
984
+ set: () => Promise.resolve(),
985
+ getFetchError: (keyId) => Promise.resolve(keyId.href === missing(1) ? { error: new FetchError(keyId, "Cached failure") } : void 0),
986
+ setFetchError: () => Promise.resolve()
987
+ },
988
+ gets
989
+ };
990
+ };
991
+ await t.step("refreshes a stale cached key once, only for its signature", async () => {
992
+ const { keyCache, gets } = createStaleKeyCache();
993
+ const documentLoader = createCountingKeyLoader();
994
+ const result = await verifyRequestDetailed(await signRfc9421Many([
995
+ missing(1),
996
+ missing(2),
997
+ forged,
998
+ valid
999
+ ], currentTime), {
1000
+ documentLoader,
1001
+ contextLoader: mockDocumentLoader,
1002
+ currentTime,
1003
+ keyCache,
1004
+ maxSignatures: 4
1005
+ });
1006
+ assert(result.verified);
1007
+ assertEquals(result.key, rsaPublicKey2);
1008
+ assertEquals(result.signatureLabel, "sig4");
1009
+ assertEquals(documentLoader.loaded, [missing(2), valid]);
1010
+ assertEquals(gets, [
1011
+ missing(1),
1012
+ missing(2),
1013
+ valid
1014
+ ]);
1015
+ });
1016
+ await t.step("refreshes each stale cached key at most once", async () => {
1017
+ const stale = (n) => `https://example.com/stale-${n}`;
1018
+ const gets = [];
1019
+ const keyCache = {
1020
+ get(keyId) {
1021
+ gets.push(keyId.href);
1022
+ return Promise.resolve(rsaPublicKey1);
1023
+ },
1024
+ set: () => Promise.resolve()
1025
+ };
1026
+ const documentLoader = createCountingKeyLoader();
1027
+ const result = await verifyRequestDetailed(await signRfc9421Many([
1028
+ stale(1),
1029
+ stale(2),
1030
+ stale(1),
1031
+ stale(4)
1032
+ ], currentTime), {
1033
+ documentLoader,
1034
+ contextLoader: mockDocumentLoader,
1035
+ currentTime,
1036
+ keyCache
1037
+ });
1038
+ assertFalse(result.verified);
1039
+ assertEquals(result.reason.type, "keyFetchError");
1040
+ assertEquals("keyId" in result.reason ? result.reason.keyId?.href : void 0, stale(1));
1041
+ assertEquals(documentLoader.loaded, [stale(1), stale(2)]);
1042
+ assertEquals(gets, [stale(1), stale(2)]);
1043
+ });
1044
+ await t.step("does not refresh a cached key for other failures", async () => {
1045
+ const [input] = Object.values(splitRfc9421Header((await signRfc9421Many([valid], currentTime)).headers.get("Signature-Input")));
1046
+ const withAlg = (alg) => /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, `;alg="${alg}"`) : `${input};alg="${alg}"`;
1047
+ for (const signatureInput of [withAlg("unknown"), input.replace(/^sig1=\([^)]*\)/, "sig1=(\"@unsupported\")")]) {
1048
+ const request = await signRfc9421Many([valid], currentTime);
1049
+ request.headers.set("Signature-Input", signatureInput);
1050
+ const { keyCache } = createStaleKeyCache();
1051
+ const documentLoader = createCountingKeyLoader();
1052
+ assertEquals(await verifyRequest(request, {
1053
+ documentLoader,
1054
+ contextLoader: mockDocumentLoader,
1055
+ currentTime,
1056
+ keyCache
1057
+ }), null, signatureInput);
1058
+ assertEquals(documentLoader.loaded, [], signatureInput);
1059
+ }
1060
+ });
1061
+ await t.step("refreshes a cached key of another algorithm family", async () => {
1062
+ const keyId = ed25519PublicKey.id.href;
1063
+ const request = await signRfc9421Many([keyId], currentTime);
1064
+ const input = request.headers.get("Signature-Input");
1065
+ request.headers.set("Signature-Input", /;alg="[^"]*"/.test(input) ? input.replace(/;alg="[^"]*"/, ";alg=\"ed25519\"") : `${input};alg="ed25519"`);
1066
+ const { sig1 } = parseRfc9421SignatureInput(request.headers.get("Signature-Input"));
1067
+ const signature = await crypto.subtle.sign("Ed25519", ed25519PrivateKey, new TextEncoder().encode(createRfc9421SignatureBase(request, sig1.components, sig1.parameters)));
1068
+ request.headers.set("Signature", `sig1=:${encodeBase64(new Uint8Array(signature))}:`);
1069
+ const loaded = [];
1070
+ const result = await verifyRequestDetailed(request, {
1071
+ documentLoader: (url, options) => {
1072
+ loaded.push(url);
1073
+ return mockDocumentLoader(url, options);
1074
+ },
1075
+ contextLoader: mockDocumentLoader,
1076
+ currentTime,
1077
+ keyCache: {
1078
+ get: (id) => Promise.resolve(id.href === keyId ? rsaPublicKey1 : void 0),
1079
+ set: () => Promise.resolve()
1080
+ }
1081
+ });
1082
+ assert(result.verified);
1083
+ assertEquals(result.key.id?.href, keyId);
1084
+ assertEquals(loaded.length, 1);
1085
+ });
1086
+ });
631
1087
  test("verifyRequest() [rfc9421] manual POST verification", async () => {
632
1088
  const currentTimestamp = 1709626184;
633
1089
  const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
@@ -737,7 +1193,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
737
1193
  contextLoader: mockDocumentLoader,
738
1194
  documentLoader: mockDocumentLoader,
739
1195
  spec: "rfc9421",
740
- currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 2592e3) * 1e3)).toISOString()}`),
1196
+ currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1712218184 * 1e3)).toISOString()}`),
741
1197
  timeWindow: { hours: 1 }
742
1198
  }), null, "Should fail verification when signature timestamp is too old");
743
1199
  assertEquals(await verifyRequest(new Request("https://example.com/api/resource", {
@@ -769,7 +1225,7 @@ test("verifyRequest() [rfc9421] error cases and edge cases", async () => {
769
1225
  contextLoader: mockDocumentLoader,
770
1226
  documentLoader: mockDocumentLoader,
771
1227
  spec: "rfc9421",
772
- currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date((currentTimestamp + 31536e3) * 1e3)).toISOString()}`),
1228
+ currentTime: Temporal.Instant.from(`${(/* @__PURE__ */ new Date(1741162184 * 1e3)).toISOString()}`),
773
1229
  timeWindow: false
774
1230
  }), rsaPublicKey2, "Should verify signature when time checking is disabled");
775
1231
  const freshSignedPostRequest = await signRequest(new Request("https://example.com/api/resource", {
@@ -1001,6 +1457,7 @@ test("doubleKnock() function with redirect handling", async () => {
1001
1457
  esm_default.spyGlobal();
1002
1458
  const requestedUrls = [];
1003
1459
  const responseCodes = [];
1460
+ const validatedRedirects = [];
1004
1461
  esm_default.post("https://example.com/redirect-endpoint", (cl) => {
1005
1462
  requestedUrls.push(cl.url);
1006
1463
  responseCodes.push(302);
@@ -1018,11 +1475,41 @@ test("doubleKnock() function with redirect handling", async () => {
1018
1475
  }), {
1019
1476
  keyId: rsaPublicKey2.id,
1020
1477
  privateKey: rsaPrivateKey2
1021
- })).status, 202, "Final response status should be 202 Accepted");
1478
+ }, { validateRedirect(url) {
1479
+ validatedRedirects.push(url);
1480
+ } })).status, 202, "Final response status should be 202 Accepted");
1022
1481
  assertEquals(requestedUrls.length, 2, "Two URLs should have been requested");
1023
1482
  assertEquals(requestedUrls[0], "https://example.com/redirect-endpoint", "First request should be to redirect-endpoint");
1024
1483
  assertEquals(requestedUrls[1], "https://example.com/final-endpoint", "Second request should be to final-endpoint");
1025
1484
  assertEquals(responseCodes, [302, 202], "Response status codes should match expected sequence");
1485
+ assertEquals(validatedRedirects, ["https://example.com/final-endpoint"]);
1486
+ esm_default.hardReset();
1487
+ });
1488
+ test("doubleKnock() validates redirects after signature fallback", async () => {
1489
+ esm_default.spyGlobal();
1490
+ let requestCount = 0;
1491
+ let privateRequestCount = 0;
1492
+ esm_default.post("https://example.com/redirect-after-fallback", (cl) => {
1493
+ requestCount++;
1494
+ if (cl.request.headers.has("Signature-Input")) return new Response("Unauthorized", { status: 401 });
1495
+ return Response.redirect("http://localhost/private", 302);
1496
+ });
1497
+ esm_default.post("http://localhost/private", () => {
1498
+ privateRequestCount++;
1499
+ return new Response("", { status: 202 });
1500
+ });
1501
+ const request = new Request("https://example.com/redirect-after-fallback", {
1502
+ method: "POST",
1503
+ body: "Test message"
1504
+ });
1505
+ await assertRejects(() => doubleKnock(request, {
1506
+ keyId: rsaPublicKey2.id,
1507
+ privateKey: rsaPrivateKey2
1508
+ }, { validateRedirect(url) {
1509
+ throw new Error(`Disallowed redirect: ${url}`);
1510
+ } }), Error, "Disallowed redirect: http://localhost/private");
1511
+ assertEquals(requestCount, 2);
1512
+ assertEquals(privateRequestCount, 0);
1026
1513
  esm_default.hardReset();
1027
1514
  });
1028
1515
  test("doubleKnock() function with both specs rejected", async () => {
@@ -1813,6 +2300,37 @@ test("doubleKnock(): Accept-Signature challenge retry succeeds", async () => {
1813
2300
  assertEquals(requestCount, 2);
1814
2301
  esm_default.hardReset();
1815
2302
  });
2303
+ test("doubleKnock(): validates redirects after Accept-Signature challenge retry", async () => {
2304
+ esm_default.spyGlobal();
2305
+ let requestCount = 0;
2306
+ let privateRequestCount = 0;
2307
+ esm_default.post("https://example.com/inbox-challenge-redirect", () => {
2308
+ requestCount++;
2309
+ if (requestCount === 1) return new Response("Not Authorized", {
2310
+ status: 401,
2311
+ headers: { "Accept-Signature": "sig1=(\"@method\" \"@target-uri\" \"@authority\" \"content-digest\");created;nonce=\"challenge-nonce-redirect\"" }
2312
+ });
2313
+ return Response.redirect("http://localhost/private", 302);
2314
+ });
2315
+ esm_default.post("http://localhost/private", () => {
2316
+ privateRequestCount++;
2317
+ return new Response("", { status: 202 });
2318
+ });
2319
+ const request = new Request("https://example.com/inbox-challenge-redirect", {
2320
+ method: "POST",
2321
+ body: "Test message",
2322
+ headers: { "Content-Type": "text/plain" }
2323
+ });
2324
+ await assertRejects(() => doubleKnock(request, {
2325
+ keyId: rsaPublicKey2.id,
2326
+ privateKey: rsaPrivateKey2
2327
+ }, { validateRedirect(url) {
2328
+ throw new Error(`Disallowed redirect: ${url}`);
2329
+ } }), Error, "Disallowed redirect: http://localhost/private");
2330
+ assertEquals(requestCount, 2);
2331
+ assertEquals(privateRequestCount, 0);
2332
+ esm_default.hardReset();
2333
+ });
1816
2334
  test("doubleKnock(): unfulfillable Accept-Signature falls to legacy fallback", async () => {
1817
2335
  esm_default.spyGlobal();
1818
2336
  let requestCount = 0;
@@ -2009,5 +2527,158 @@ test("doubleKnock(): Accept-Signature with multiple compatible entries fulfills
2009
2527
  assertEquals(requestCount, 2);
2010
2528
  esm_default.hardReset();
2011
2529
  });
2530
+ test("listRequestSignatures()", async (t) => {
2531
+ const request = () => new Request("https://example.com/inbox", {
2532
+ method: "POST",
2533
+ body: "Test message"
2534
+ });
2535
+ await t.step("draft-cavage", async () => {
2536
+ const signed = await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id);
2537
+ const signatures = listRequestSignatures(signed);
2538
+ assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, null]]);
2539
+ const components = signatures[0].components;
2540
+ assertEquals(components.map((c) => c.value), signed.headers.get("Signature").match(/headers="([^"]*)"/)[1].split(" "));
2541
+ assert(components.every((c) => Object.keys(c.params).length < 1));
2542
+ });
2543
+ await t.step("RFC 9421", async () => {
2544
+ const signatures = listRequestSignatures(await signRequest(request(), rsaPrivateKey2, rsaPublicKey2.id, {
2545
+ spec: "rfc9421",
2546
+ rfc9421: { label: "a" }
2547
+ }));
2548
+ assertEquals(signatures.map((s) => [s.keyId.href, s.label]), [[rsaPublicKey2.id.href, "a"]]);
2549
+ assertEquals(signatures[0].components.map((c) => c.value), [
2550
+ "@method",
2551
+ "@target-uri",
2552
+ "@authority",
2553
+ "host",
2554
+ "date",
2555
+ "content-digest"
2556
+ ]);
2557
+ });
2558
+ await t.step("RFC 9421 beyond the limit", async () => {
2559
+ const currentTime = Temporal.Instant.from("2024-03-05T08:09:44Z");
2560
+ const signed = await signRfc9421Many([
2561
+ 1,
2562
+ 2,
2563
+ 3,
2564
+ 4
2565
+ ].map((n) => `https://example.com/key-${n}`), currentTime);
2566
+ const headers = new Headers(signed.headers);
2567
+ headers.set("Signature-Input", headers.get("Signature-Input").replace("https://example.com/key-1", "not a URL"));
2568
+ const request = new Request(signed.url, {
2569
+ method: "POST",
2570
+ headers
2571
+ });
2572
+ const keyIds = (maxSignatures) => listRequestSignatures(request, maxSignatures).map((s) => s.keyId.href);
2573
+ assertEquals(keyIds(), ["https://example.com/key-2", "https://example.com/key-3"]);
2574
+ assertEquals(keyIds(1), []);
2575
+ assertEquals(keyIds(Infinity), [
2576
+ "https://example.com/key-2",
2577
+ "https://example.com/key-3",
2578
+ "https://example.com/key-4"
2579
+ ]);
2580
+ });
2581
+ await t.step("unsigned or malformed", () => {
2582
+ assertEquals(listRequestSignatures(request()), []);
2583
+ const malformed = request();
2584
+ malformed.headers.set("Signature", "keyId=\"not a URL\",signature=\"AA==\"");
2585
+ assertEquals(listRequestSignatures(malformed), []);
2586
+ const garbage = request();
2587
+ garbage.headers.set("Signature-Input", "((");
2588
+ garbage.headers.set("Signature", "((");
2589
+ assertEquals(listRequestSignatures(garbage), []);
2590
+ });
2591
+ });
2592
+ test("selectRequestSignature()", async () => {
2593
+ const body = "Test message";
2594
+ const sign = (label, keyId) => signRequest(new Request("https://example.com/inbox", {
2595
+ method: "POST",
2596
+ body
2597
+ }), rsaPrivateKey2, keyId, {
2598
+ spec: "rfc9421",
2599
+ rfc9421: { label }
2600
+ });
2601
+ const first = await sign("a", new URL("https://example.com/other#key"));
2602
+ const second = await sign("b", rsaPublicKey2.id);
2603
+ const headers = new Headers(second.headers);
2604
+ for (const name of ["Signature-Input", "Signature"]) headers.set(name, `${first.headers.get(name)}, ${second.headers.get(name)}`);
2605
+ const request = new Request(second.url, {
2606
+ method: "POST",
2607
+ headers,
2608
+ body
2609
+ });
2610
+ const signatures = listRequestSignatures(request);
2611
+ assertEquals(signatures.map((s) => s.label), ["a", "b"]);
2612
+ const selected = selectRequestSignature(request, signatures[1]);
2613
+ assertEquals(listRequestSignatures(selected).map((s) => [s.label, s.keyId.href]), [["b", rsaPublicKey2.id.href]]);
2614
+ const result = await verifyRequestDetailed(selected, {
2615
+ documentLoader: mockDocumentLoader,
2616
+ contextLoader: mockDocumentLoader
2617
+ });
2618
+ assert(result.verified);
2619
+ assertEquals(result.key.id, rsaPublicKey2.id);
2620
+ assertEquals(await selected.text(), body);
2621
+ assertEquals(await request.text(), body);
2622
+ });
2623
+ test("verifyRequestDetailed() reports a timed-out key fetch", async (t) => {
2624
+ for (const stall of ["headers", "body"]) await t.step(`stalled ${stall}`, async () => {
2625
+ esm_default.mockGlobal();
2626
+ const keyId = new URL(`https://slow.example/actors/alice#${stall}`);
2627
+ let requests = 0;
2628
+ esm_default.get(`begin:https://slow.example/actors/alice`, () => {
2629
+ requests++;
2630
+ if (stall === "headers") return new Promise(() => {});
2631
+ return new Response(new ReadableStream({ pull: () => new Promise(() => {}) }), { status: 404 });
2632
+ });
2633
+ const loader = getDocumentLoader({
2634
+ allowPrivateAddress: true,
2635
+ timeout: 100
2636
+ });
2637
+ const documentLoader = (url, options) => url.startsWith("https://slow.example/") ? loader(url, options) : mockDocumentLoader(url);
2638
+ const kv = new MemoryKvStore();
2639
+ try {
2640
+ const sign = () => signRequest(new Request("https://example.com/inbox", {
2641
+ method: "POST",
2642
+ headers: {
2643
+ "Content-Type": "application/activity+json",
2644
+ accept: "application/ld+json"
2645
+ },
2646
+ body: JSON.stringify({
2647
+ "@context": "https://www.w3.org/ns/activitystreams",
2648
+ type: "Create",
2649
+ actor: "https://slow.example/actors/alice"
2650
+ })
2651
+ }), rsaPrivateKey2, keyId);
2652
+ const result = await verifyRequestDetailed(await sign(), {
2653
+ contextLoader: mockDocumentLoader,
2654
+ documentLoader,
2655
+ keyCache: new KvKeyCache(kv, ["pk"])
2656
+ });
2657
+ assertFalse(result.verified);
2658
+ assert(result.reason.type === "keyFetchError");
2659
+ assertEquals(result.reason.keyId, keyId);
2660
+ assert("error" in result.reason.result);
2661
+ const error = result.reason.result.error;
2662
+ assert(error instanceof FetchError);
2663
+ assertEquals(error.response, void 0);
2664
+ assert(error.cause instanceof DOMException);
2665
+ assertEquals(error.cause.name, "TimeoutError");
2666
+ assertEquals(requests, 1);
2667
+ const cached = await verifyRequestDetailed(await sign(), {
2668
+ contextLoader: mockDocumentLoader,
2669
+ documentLoader,
2670
+ keyCache: new KvKeyCache(kv, ["pk"])
2671
+ });
2672
+ assertFalse(cached.verified);
2673
+ assert(cached.reason.type === "keyFetchError");
2674
+ assert("error" in cached.reason.result);
2675
+ assertEquals(cached.reason.result.error.name, "FetchError");
2676
+ assertStringIncludes(cached.reason.result.error.message, "Timed out after 100 ms");
2677
+ assertEquals(requests, 1);
2678
+ } finally {
2679
+ esm_default.hardReset();
2680
+ }
2681
+ });
2682
+ });
2012
2683
  //#endregion
2013
2684
  export {};