acrylblends 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/dist/index.d.ts +22 -16
  2. package/dist/index.js +65 -20
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/acryl-inspect/LICENSE +21 -0
  6. package/plugins/acryl-inspect/plugin.json +12 -0
  7. package/plugins/acryl-inspect/src/index.ts +3 -0
  8. package/plugins/acryl-inspect/src/report.ts +126 -0
  9. package/plugins/acryl-inspect/src/service.ts +68 -0
  10. package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
  11. package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
  12. package/plugins/acryl-inspect/upstream.json +18 -0
  13. package/plugins/acryl-node-server/LICENSE +47 -0
  14. package/plugins/acryl-node-server/plugin.json +13 -0
  15. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  16. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  17. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  18. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  19. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  20. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  21. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  22. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  23. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  24. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  25. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  26. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  27. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  28. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  29. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  30. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  31. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  32. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  33. package/plugins/acryl-node-server/src/index.ts +107 -0
  34. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  35. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  36. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  37. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  38. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  39. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  40. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  41. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  42. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  43. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  44. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  45. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  46. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  47. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  48. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  49. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  50. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  51. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  52. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  53. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  54. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  55. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  56. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  57. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  58. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  59. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  60. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  61. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  62. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  63. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  64. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  65. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  66. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  67. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  68. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  69. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  70. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  71. package/plugins/acryl-node-server/tests/server.spec.ts +141 -0
  72. package/plugins/acryl-node-server/upstream.json +18 -0
  73. package/plugins/acryl-router/LICENSE +47 -0
  74. package/plugins/acryl-router/plugin.json +13 -0
  75. package/plugins/acryl-router/src/client.ts +2 -0
  76. package/plugins/acryl-router/src/connection.ts +42 -0
  77. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  78. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  79. package/plugins/acryl-router/src/core/app.ts +34 -0
  80. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  81. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  82. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  83. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  84. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  85. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  86. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  87. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  88. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  89. package/plugins/acryl-router/src/core/compose.ts +73 -0
  90. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  91. package/plugins/acryl-router/src/core/context.ts +781 -0
  92. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  93. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  94. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  95. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  96. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  97. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  98. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  99. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  100. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  101. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  102. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  103. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  104. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  105. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  106. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  107. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  108. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  109. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  110. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  111. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  112. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  113. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  114. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  115. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  116. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  117. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  118. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  119. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  120. package/plugins/acryl-router/src/core/index.ts +53 -0
  121. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  122. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  123. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  124. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  125. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  126. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  127. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  128. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  129. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  130. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  131. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  132. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  133. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  134. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  135. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  136. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  137. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  138. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  139. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  140. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  141. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  142. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  143. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  144. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  145. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  146. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  147. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  148. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  149. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  150. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  151. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  152. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  153. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  154. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  156. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  157. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  158. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  159. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  160. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  161. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  162. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  163. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  164. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  165. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  166. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  167. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  168. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  169. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  170. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  171. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  172. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  173. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  174. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  175. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  176. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  177. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  178. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  179. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  180. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  181. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  182. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  183. package/plugins/acryl-router/src/core/request.ts +502 -0
  184. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  185. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  186. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  187. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  188. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  189. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  190. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  191. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  192. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  193. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  194. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  195. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  196. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  197. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  198. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  199. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  200. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  201. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  202. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  203. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  204. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  205. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  206. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  207. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  208. package/plugins/acryl-router/src/core/router.ts +103 -0
  209. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  210. package/plugins/acryl-router/src/core/types.ts +2782 -0
  211. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  212. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  213. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  214. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  215. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  216. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  217. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  218. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  219. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  220. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  221. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  222. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  223. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  224. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  225. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  226. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  227. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  228. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  229. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  230. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  231. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  232. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  233. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  234. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  235. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  236. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  237. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  238. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  239. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  240. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  241. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  242. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  243. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  244. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  245. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  246. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  247. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  248. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  249. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  250. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  251. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  252. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  253. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  254. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  255. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  256. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  257. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  258. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  259. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  260. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  261. package/plugins/acryl-router/src/helpers/index.ts +13 -0
  262. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  263. package/plugins/acryl-router/src/index.ts +6 -0
  264. package/plugins/acryl-router/src/middleware/index.ts +25 -0
  265. package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
  266. package/plugins/acryl-router/src/service.ts +356 -0
  267. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  268. package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
  269. package/plugins/acryl-router/tests/loader.spec.ts +72 -0
  270. package/plugins/acryl-router/tests/service.spec.ts +409 -0
  271. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  272. package/plugins/acryl-router/upstream.json +20 -0
  273. package/plugins/acryl-router-http/LICENSE +21 -0
  274. package/plugins/acryl-router-http/plugin.json +12 -0
  275. package/plugins/acryl-router-http/src/bridge.ts +54 -0
  276. package/plugins/acryl-router-http/src/index.ts +63 -0
  277. package/plugins/acryl-router-http/src/serve.ts +24 -0
  278. package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
  279. package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
  280. package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
  281. package/plugins/acryl-router-http/upstream.json +22 -0
  282. package/plugins/acryl-state/LICENSE +21 -0
  283. package/plugins/acryl-state/NOTICE +30 -0
  284. package/plugins/acryl-state/plugin.json +12 -0
  285. package/plugins/acryl-state/src/cell.ts +67 -0
  286. package/plugins/acryl-state/src/index.ts +3 -0
  287. package/plugins/acryl-state/src/react.ts +11 -0
  288. package/plugins/acryl-state/src/service.ts +119 -0
  289. package/plugins/acryl-state/tests/cell.spec.ts +71 -0
  290. package/plugins/acryl-state/tests/react.spec.ts +39 -0
  291. package/plugins/acryl-state/tests/service.spec.ts +132 -0
  292. package/plugins/acryl-state/upstream.json +22 -0
  293. package/plugins/builder/script.ts +6 -10
  294. package/plugins/extensions/index.ts +20 -3
  295. package/plugins/extensions/resolve.ts +28 -3
  296. package/plugins/openai/prompt.ts +12 -6
  297. package/plugins/tasks/index.ts +14 -0
  298. package/plugins/tsconfig.json +14 -5
  299. package/templates/agent/blend.yaml +5 -7
  300. package/templates/agent/src/app.ts +8 -8
  301. package/templates/agent/src/routes.ts +39 -38
  302. package/templates/agent/template.json +3 -3
  303. package/templates/agent/test/builder.test.ts +22 -14
  304. package/templates/agent/test/model.test.ts +3 -3
  305. package/templates/agent/tsconfig.json +8 -5
  306. package/templates/{app → blank}/blend.yaml +3 -3
  307. package/templates/{app → blank}/src/app.ts +5 -5
  308. package/templates/blank/src/routes.ts +16 -0
  309. package/templates/blank/template.json +1 -0
  310. package/templates/{app → blank}/test/app.test.ts +1 -1
  311. package/templates/{app → blank}/tsconfig.json +2 -4
  312. package/templates/todo/README.md +27 -0
  313. package/templates/todo/blend.yaml +25 -0
  314. package/templates/todo/gitignore +2 -0
  315. package/templates/todo/package.json +23 -0
  316. package/templates/todo/src/app.ts +71 -0
  317. package/templates/todo/src/main.ts +19 -0
  318. package/templates/todo/src/page.ts +28 -0
  319. package/templates/todo/src/todo.ts +123 -0
  320. package/templates/todo/template.json +1 -0
  321. package/templates/todo/test/todo.test.ts +145 -0
  322. package/templates/todo/tsconfig.json +17 -0
  323. package/templates/todo/upstream.json +12 -0
  324. package/plugins/events/index.ts +0 -42
  325. package/plugins/events/plugin.json +0 -9
  326. package/plugins/http/index.ts +0 -64
  327. package/plugins/http/plugin.json +0 -10
  328. package/plugins/server/index.ts +0 -138
  329. package/plugins/server/plugin.json +0 -10
  330. package/templates/app/src/routes.ts +0 -14
  331. package/templates/app/template.json +0 -1
  332. /package/templates/{app → blank}/README.md +0 -0
  333. /package/templates/{app → blank}/gitignore +0 -0
  334. /package/templates/{app → blank}/package.json +0 -0
  335. /package/templates/{app → blank}/src/main.ts +0 -0
@@ -0,0 +1,1788 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { vi } from 'vitest'
3
+ import { encodeBase64, encodeBase64Url } from '../encode.ts'
4
+ import { AlgorithmTypes } from './jwa.ts'
5
+ import type { AppJsonWebKey } from './jws.ts'
6
+ import { signing } from './jws.ts'
7
+ import * as JWT from './jwt.ts'
8
+ import { verifyWithJwks } from './jwt.ts'
9
+ import {
10
+ JwtAlgorithmMismatch,
11
+ JwtAlgorithmNotAllowed,
12
+ JwtAlgorithmNotImplemented,
13
+ JwtAlgorithmRequired,
14
+ JwtPayloadRequiresAud,
15
+ JwtSymmetricAlgorithmNotAllowed,
16
+ JwtTokenAudience,
17
+ JwtTokenExpired,
18
+ JwtTokenInvalid,
19
+ JwtTokenIssuedAt,
20
+ JwtTokenIssuer,
21
+ JwtTokenNotBefore,
22
+ JwtTokenSignatureMismatched,
23
+ } from './types.ts'
24
+ import { utf8Encoder } from './utf8.ts'
25
+
26
+ describe('isTokenHeader', () => {
27
+ it('should return true for valid TokenHeader', () => {
28
+ const validTokenHeader: JWT.TokenHeader = {
29
+ alg: AlgorithmTypes.HS256,
30
+ typ: 'JWT',
31
+ }
32
+
33
+ expect(JWT.isTokenHeader(validTokenHeader)).toBe(true)
34
+ })
35
+
36
+ it('should return false for invalid TokenHeader', () => {
37
+ const invalidTokenHeader = {
38
+ alg: 'invalid',
39
+ typ: 'JWT',
40
+ }
41
+
42
+ expect(JWT.isTokenHeader(invalidTokenHeader)).toBe(false)
43
+ })
44
+
45
+ it('returns true even if the typ field is absent in a TokenHeader', () => {
46
+ const validTokenHeader: JWT.TokenHeader = {
47
+ alg: AlgorithmTypes.HS256,
48
+ }
49
+
50
+ expect(JWT.isTokenHeader(validTokenHeader)).toBe(true)
51
+ })
52
+
53
+ it('returns false when the typ field is present but empty', () => {
54
+ const invalidTokenHeader = {
55
+ alg: AlgorithmTypes.HS256,
56
+ typ: '',
57
+ }
58
+
59
+ expect(JWT.isTokenHeader(invalidTokenHeader)).toBe(false)
60
+ })
61
+ })
62
+
63
+ describe('JWT', () => {
64
+ it('JwtAlgorithmNotImplemented', async () => {
65
+ const payload = { message: 'hello world' }
66
+ const secret = 'a-secret'
67
+ const alg = ''
68
+ let tok = ''
69
+ let err: JwtAlgorithmNotImplemented
70
+ try {
71
+ tok = await JWT.sign(payload, secret, alg as AlgorithmTypes)
72
+ } catch (e) {
73
+ err = e as JwtAlgorithmNotImplemented
74
+ }
75
+ expect(tok).toBe('')
76
+ // @ts-ignore
77
+ expect(err).toEqual(new JwtAlgorithmNotImplemented(alg))
78
+ })
79
+
80
+ it('JwtTokenInvalid', async () => {
81
+ const tok = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ'
82
+ const secret = 'a-secret'
83
+ let err: JwtTokenInvalid
84
+ let authorized
85
+ try {
86
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
87
+ } catch (e) {
88
+ err = e as JwtTokenInvalid
89
+ }
90
+ // @ts-ignore
91
+ expect(err).toEqual(new JwtTokenInvalid(tok))
92
+ expect(authorized).toBeUndefined()
93
+ })
94
+
95
+ it('JwtTokenInvalid for a signature that is not valid base64url', async () => {
96
+ const tok = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.@@@@'
97
+ const err = await JWT.verify(tok, 'a-secret', AlgorithmTypes.HS256).catch((e: unknown) => e)
98
+ expect(err).toBeInstanceOf(JwtTokenInvalid)
99
+ expect(err).toHaveProperty('message', `invalid JWT token: ${tok}`)
100
+ })
101
+
102
+ it('JwtTokenSignatureMismatched for an empty signature', async () => {
103
+ const tok = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.'
104
+ const err = await JWT.verify(tok, 'a-secret', AlgorithmTypes.HS256).catch((e: unknown) => e)
105
+ expect(err).toBeInstanceOf(JwtTokenSignatureMismatched)
106
+ expect(err).toHaveProperty('message', `token(${tok}) signature mismatched`)
107
+ })
108
+
109
+ it('JwtTokenNotBefore', async () => {
110
+ const tok =
111
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NjQ2MDYzMzQsImV4cCI6MTY2NDYwOTkzNCwibmJmIjoiMzEwNDYwNjI2NCJ9.hpSDT_cfkxeiLWEpWVT8TDxFP3dFi27q1K7CcMcLXHc'
112
+ const secret = 'a-secret'
113
+ let err: JwtTokenNotBefore
114
+ let authorized
115
+ try {
116
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
117
+ } catch (e) {
118
+ err = e as JwtTokenNotBefore
119
+ }
120
+ // @ts-ignore
121
+ expect(err).toEqual(new JwtTokenNotBefore(tok))
122
+ expect(authorized).toBeUndefined()
123
+ })
124
+
125
+ describe('JwtTokenNotBefore with malformed nbf claim', () => {
126
+ it('rejects token with nbf as a non-numeric string', async () => {
127
+ const secret = 'a-secret'
128
+ const tok = await JWT.sign(
129
+ // @ts-expect-error - testing malformed payload (nbf must be number)
130
+ { message: 'hello', nbf: 'tomorrow' },
131
+ secret,
132
+ AlgorithmTypes.HS256
133
+ )
134
+
135
+ let err
136
+ let authorized
137
+ try {
138
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
139
+ } catch (e) {
140
+ err = e
141
+ }
142
+ expect(err).toEqual(new JwtTokenNotBefore(tok))
143
+ expect(authorized).toBeUndefined()
144
+ })
145
+
146
+ it('rejects token with nbf = Infinity (parsed from 1e400 in JSON)', async () => {
147
+ // JSON.stringify converts Infinity to null, so hand-craft the payload.
148
+ const secret = 'a-secret'
149
+ const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
150
+ const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
151
+ const encodedPayload = encode('{"message":"hello","nbf":1e400}')
152
+ const signingInput = `${encodedHeader}.${encodedPayload}`
153
+ const signatureBuffer = await signing(
154
+ secret,
155
+ AlgorithmTypes.HS256,
156
+ utf8Encoder.encode(signingInput)
157
+ )
158
+ const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
159
+
160
+ let err
161
+ let authorized
162
+ try {
163
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
164
+ } catch (e) {
165
+ err = e
166
+ }
167
+ expect(err).toEqual(new JwtTokenNotBefore(tok))
168
+ expect(authorized).toBeUndefined()
169
+ })
170
+ })
171
+
172
+ it('JwtTokenExpired', async () => {
173
+ const tok =
174
+ 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzMwNDYxMDAsImV4cCI6MTYzMzA0NjQwMH0.H-OI1TWAbmK8RonvcpPaQcNvOKS9sxinEOsgKwjoiVo'
175
+ const secret = 'a-secret'
176
+ let err
177
+ let authorized
178
+ try {
179
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
180
+ } catch (e) {
181
+ err = e
182
+ }
183
+ expect(err).toEqual(new JwtTokenExpired(tok))
184
+ expect(authorized).toBeUndefined()
185
+ })
186
+
187
+ it('JwtTokenExpired after Y2038', async () => {
188
+ const postY2038 = 2147483648 // 2038-01-19 03:14:08 UTC
189
+ vi.useFakeTimers().setSystemTime(new Date(postY2038 * 1000))
190
+
191
+ const expIn2025 = 1735689600 // 2025-01-01 00:00:00 UTC
192
+ const payload = { message: 'hello', exp: expIn2025 }
193
+ const secret = 'a-secret'
194
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
195
+
196
+ let err
197
+ let authorized
198
+ try {
199
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
200
+ } catch (e) {
201
+ err = e
202
+ }
203
+ expect(err).toEqual(new JwtTokenExpired(tok))
204
+ expect(authorized).toBeUndefined()
205
+
206
+ vi.useRealTimers()
207
+ })
208
+
209
+ describe('JwtTokenExpired with malformed exp claim', () => {
210
+ it('rejects token with exp = 0 (epoch zero)', async () => {
211
+ const secret = 'a-secret'
212
+ const tok = await JWT.sign({ message: 'hello', exp: 0 }, secret, AlgorithmTypes.HS256)
213
+
214
+ let err
215
+ let authorized
216
+ try {
217
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
218
+ } catch (e) {
219
+ err = e
220
+ }
221
+ expect(err).toEqual(new JwtTokenExpired(tok))
222
+ expect(authorized).toBeUndefined()
223
+ })
224
+
225
+ it('rejects token with exp as a non-numeric string', async () => {
226
+ const secret = 'a-secret'
227
+ const tok = await JWT.sign(
228
+ // @ts-expect-error - testing malformed payload (exp must be number)
229
+ { message: 'hello', exp: 'tomorrow' },
230
+ secret,
231
+ AlgorithmTypes.HS256
232
+ )
233
+
234
+ let err
235
+ let authorized
236
+ try {
237
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
238
+ } catch (e) {
239
+ err = e
240
+ }
241
+ expect(err).toEqual(new JwtTokenExpired(tok))
242
+ expect(authorized).toBeUndefined()
243
+ })
244
+
245
+ it('rejects token with exp = Infinity (parsed from 1e400 in JSON)', async () => {
246
+ // JSON.stringify converts Infinity to null, so hand-craft the payload.
247
+ const secret = 'a-secret'
248
+ const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
249
+ const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
250
+ const encodedPayload = encode('{"message":"hello","exp":1e400}')
251
+ const signingInput = `${encodedHeader}.${encodedPayload}`
252
+ const signatureBuffer = await signing(
253
+ secret,
254
+ AlgorithmTypes.HS256,
255
+ utf8Encoder.encode(signingInput)
256
+ )
257
+ const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
258
+
259
+ let err
260
+ let authorized
261
+ try {
262
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
263
+ } catch (e) {
264
+ err = e
265
+ }
266
+ expect(err).toEqual(new JwtTokenExpired(tok))
267
+ expect(authorized).toBeUndefined()
268
+ })
269
+ })
270
+
271
+ it('JwtTokenIssuedAt', async () => {
272
+ const now = 1633046400
273
+ vi.useFakeTimers().setSystemTime(new Date().setTime(now * 1000))
274
+
275
+ const iat = now + 1000 // after 1s
276
+ const payload = { role: 'api_role', iat }
277
+ const secret = 'a-secret'
278
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
279
+
280
+ let err
281
+ let authorized
282
+ try {
283
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
284
+ } catch (e) {
285
+ err = e
286
+ }
287
+ expect(err).toEqual(new JwtTokenIssuedAt(now, iat))
288
+ expect(authorized).toBeUndefined()
289
+ })
290
+
291
+ describe('JwtTokenIssuedAt with malformed iat claim', () => {
292
+ it('rejects token with iat as a non-numeric string', async () => {
293
+ const now = 1633046400
294
+ vi.useFakeTimers().setSystemTime(new Date(now * 1000))
295
+
296
+ const secret = 'a-secret'
297
+ const tok = await JWT.sign(
298
+ // @ts-expect-error - testing malformed payload (iat must be number)
299
+ { message: 'hello', iat: 'tomorrow' },
300
+ secret,
301
+ AlgorithmTypes.HS256
302
+ )
303
+
304
+ let err
305
+ let authorized
306
+ try {
307
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
308
+ } catch (e) {
309
+ err = e
310
+ }
311
+ expect(err).toEqual(new JwtTokenIssuedAt(now, 'tomorrow' as unknown as number))
312
+ expect(authorized).toBeUndefined()
313
+
314
+ vi.useRealTimers()
315
+ })
316
+
317
+ it('rejects token with iat = Infinity (parsed from 1e400 in JSON)', async () => {
318
+ // JSON.stringify converts Infinity to null, so hand-craft the payload.
319
+ const now = 1633046400
320
+ vi.useFakeTimers().setSystemTime(new Date(now * 1000))
321
+
322
+ const secret = 'a-secret'
323
+ const encode = (s: string) => encodeBase64Url(utf8Encoder.encode(s).buffer).replace(/=/g, '')
324
+ const encodedHeader = encode('{"alg":"HS256","typ":"JWT"}')
325
+ const encodedPayload = encode('{"message":"hello","iat":1e400}')
326
+ const signingInput = `${encodedHeader}.${encodedPayload}`
327
+ const signatureBuffer = await signing(
328
+ secret,
329
+ AlgorithmTypes.HS256,
330
+ utf8Encoder.encode(signingInput)
331
+ )
332
+ const tok = `${signingInput}.${encodeBase64Url(signatureBuffer).replace(/=/g, '')}`
333
+
334
+ let err
335
+ let authorized
336
+ try {
337
+ authorized = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
338
+ } catch (e) {
339
+ err = e
340
+ }
341
+ expect(err).toEqual(new JwtTokenIssuedAt(now, Infinity))
342
+ expect(authorized).toBeUndefined()
343
+
344
+ vi.useRealTimers()
345
+ })
346
+ })
347
+
348
+ it('JwtTokenIssuer (none)', async () => {
349
+ const tok =
350
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDB9.Ha3tPZzmnLGyFfZYd7GSV0iCn2F9kbZffFVZcTe5kJo'
351
+ const secret = 'a-secret'
352
+ let err
353
+ let authorized
354
+ try {
355
+ authorized = await JWT.verify(tok, secret, {
356
+ alg: AlgorithmTypes.HS256,
357
+ iss: 'some',
358
+ })
359
+ } catch (e) {
360
+ err = e
361
+ }
362
+ expect(err).toEqual(new JwtTokenIssuer('some', null))
363
+ expect(authorized).toBeUndefined()
364
+ })
365
+
366
+ it('JwtTokenIssuer (wrong - string)', async () => {
367
+ const tok =
368
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDAsImlzcyI6ImZha2UtaXNzdWVyIn0.miyPU40DBvhxpAUsndssJOMBsP1aqc8JClGnriPHfXk'
369
+ const secret = 'a-secret'
370
+ let err
371
+ let authorized
372
+ try {
373
+ authorized = await JWT.verify(tok, secret, {
374
+ alg: AlgorithmTypes.HS256,
375
+ iss: 'expected-issuer',
376
+ })
377
+ } catch (e) {
378
+ err = e
379
+ }
380
+ expect(err).toEqual(new JwtTokenIssuer('expected-issuer', 'fake-issuer'))
381
+ expect(authorized).toBeUndefined()
382
+ })
383
+
384
+ it('JwtTokenIssuer (wrong - RegExp)', async () => {
385
+ const tok =
386
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDAsImlzcyI6ImhleSJ9.Q0NJwoj-meWy42pFFrPNlREe2lOagWioJUjR4eJCx0k'
387
+ const secret = 'a-secret'
388
+ let err
389
+ let authorized
390
+ try {
391
+ authorized = await JWT.verify(tok, secret, {
392
+ alg: AlgorithmTypes.HS256,
393
+ iss: /^(hello|hi)$/,
394
+ })
395
+ } catch (e) {
396
+ err = e
397
+ }
398
+ expect(err).toEqual(new JwtTokenIssuer(/^(hello|hi)$/, 'hey'))
399
+ expect(authorized).toBeUndefined()
400
+ })
401
+
402
+ it('JwtTokenIssuer (correct - string)', async () => {
403
+ const tok =
404
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDAsImlzcyI6ImNvcnJlY3QtaXNzdWVyIn0.gF8S6M2QcfTTscgxeyihNk28JAOa8mfL1bXPb3_E3rk'
405
+ const secret = 'a-secret'
406
+ let err
407
+ let authorized
408
+ try {
409
+ authorized = await JWT.verify(tok, secret, {
410
+ alg: AlgorithmTypes.HS256,
411
+ iss: 'correct-issuer',
412
+ })
413
+ } catch (e) {
414
+ err = e
415
+ }
416
+ expect(err).toBeUndefined()
417
+ expect(authorized?.iss).toEqual('correct-issuer')
418
+ })
419
+
420
+ it('JwtTokenIssuer (correct - RegExp)', async () => {
421
+ const tok =
422
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDAsImlzcyI6ImhlbGxvIn0.5DDuValGGQu4EfS3DY7C4hwwHyTNSTD93K_YEjBzgAc'
423
+ const secret = 'a-secret'
424
+ let err
425
+ let authorized
426
+ try {
427
+ authorized = await JWT.verify(tok, secret, {
428
+ alg: AlgorithmTypes.HS256,
429
+ iss: /^(hello|hi)$/,
430
+ })
431
+ } catch (e) {
432
+ err = e
433
+ }
434
+ expect(err).toBeUndefined()
435
+ expect(authorized?.iss).toEqual('hello')
436
+ })
437
+
438
+ it('JwtPayloadRequireAud', async () => {
439
+ const tok =
440
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiaWF0IjoxfQ.3Yd0dDicCKA6zu_G6AvxMX_fRH5wMz9gMCedOsYNAGc'
441
+ const secret = 'a-secret'
442
+ let err
443
+ let authorized
444
+ try {
445
+ authorized = await JWT.verify(tok, secret, {
446
+ alg: AlgorithmTypes.HS256,
447
+ aud: 'correct-audience',
448
+ })
449
+ } catch (e) {
450
+ err = e
451
+ }
452
+ expect(err).toEqual(new JwtPayloadRequiresAud({ iss: 'https://issuer.example', iat: 1 }))
453
+ expect(authorized).toBeUndefined()
454
+ })
455
+
456
+ it('JwtTokenAudience(correct string - string)', async () => {
457
+ const tok =
458
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoiY29ycmVjdC1hdWRpZW5jZSIsImlhdCI6MX0.z8T6szX-k66de4xB9OFbpWAOfx0RTqKSUPBcdpSY5nk'
459
+ const secret = 'a-secret'
460
+ let err
461
+ let authorized
462
+ try {
463
+ authorized = await JWT.verify(tok, secret, {
464
+ alg: AlgorithmTypes.HS256,
465
+ aud: 'correct-audience',
466
+ })
467
+ } catch (e) {
468
+ err = e
469
+ }
470
+ expect(err).toBeUndefined()
471
+ expect(authorized?.aud).toEqual('correct-audience')
472
+ })
473
+
474
+ it('JwtTokenAudience(correct string - string[])', async () => {
475
+ const tok =
476
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoiY29ycmVjdC1hdWRpZW5jZSIsImlhdCI6MX0.z8T6szX-k66de4xB9OFbpWAOfx0RTqKSUPBcdpSY5nk'
477
+ const secret = 'a-secret'
478
+ let err
479
+ let authorized
480
+ try {
481
+ authorized = await JWT.verify(tok, secret, {
482
+ alg: AlgorithmTypes.HS256,
483
+ aud: ['correct-audience', 'other-audience'],
484
+ })
485
+ } catch (e) {
486
+ err = e
487
+ }
488
+ expect(err).toBeUndefined()
489
+ expect(authorized?.aud).toEqual('correct-audience')
490
+ })
491
+
492
+ it('JwtTokenAudience(correct string - RegExp)', async () => {
493
+ const tok =
494
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoiY29ycmVjdC1hdWRpZW5jZSIsImlhdCI6MX0.z8T6szX-k66de4xB9OFbpWAOfx0RTqKSUPBcdpSY5nk'
495
+ const secret = 'a-secret'
496
+ let err
497
+ let authorized
498
+ try {
499
+ authorized = await JWT.verify(tok, secret, {
500
+ alg: AlgorithmTypes.HS256,
501
+ aud: /^correct-audience$/,
502
+ })
503
+ } catch (e) {
504
+ err = e
505
+ }
506
+ expect(err).toBeUndefined()
507
+ expect(authorized?.aud).toEqual('correct-audience')
508
+ })
509
+
510
+ it('JwtTokenAudience(correct string[] - string)', async () => {
511
+ const tok =
512
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbImNvcnJlY3QtYXVkaWVuY2UiLCJvdGhlci1hdWRpZW5jZSJdLCJpYXQiOjF9.l73pNR5zMMAyuoN3f32hKtRJkoxZNzgTcVBZ2A2EsJY'
513
+ const secret = 'a-secret'
514
+ let err
515
+ let authorized
516
+ try {
517
+ authorized = await JWT.verify(tok, secret, {
518
+ alg: AlgorithmTypes.HS256,
519
+ aud: 'correct-audience',
520
+ })
521
+ } catch (e) {
522
+ err = e
523
+ }
524
+ expect(err).toBeUndefined()
525
+ expect(authorized?.aud).toEqual(['correct-audience', 'other-audience'])
526
+ })
527
+
528
+ it('JwtTokenAudience(correct string[] - string[])', async () => {
529
+ const tok =
530
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbImNvcnJlY3QtYXVkaWVuY2UiLCJvdGhlci1hdWRpZW5jZSJdLCJpYXQiOjF9.l73pNR5zMMAyuoN3f32hKtRJkoxZNzgTcVBZ2A2EsJY'
531
+ const secret = 'a-secret'
532
+ let err
533
+ let authorized
534
+ try {
535
+ authorized = await JWT.verify(tok, secret, {
536
+ alg: AlgorithmTypes.HS256,
537
+ aud: ['correct-audience', 'test'],
538
+ })
539
+ } catch (e) {
540
+ err = e
541
+ }
542
+ expect(err).toBeUndefined()
543
+ expect(authorized?.aud).toEqual(['correct-audience', 'other-audience'])
544
+ })
545
+
546
+ it('JwtTokenAudience(correct string[] - RegExp)', async () => {
547
+ const tok =
548
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbImNvcnJlY3QtYXVkaWVuY2UiLCJvdGhlci1hdWRpZW5jZSJdLCJpYXQiOjF9.l73pNR5zMMAyuoN3f32hKtRJkoxZNzgTcVBZ2A2EsJY'
549
+ const secret = 'a-secret'
550
+ let err
551
+ let authorized
552
+ try {
553
+ authorized = await JWT.verify(tok, secret, {
554
+ alg: AlgorithmTypes.HS256,
555
+ aud: /^correct-audience$/,
556
+ })
557
+ } catch (e) {
558
+ err = e
559
+ }
560
+ expect(err).toBeUndefined()
561
+ expect(authorized?.aud).toEqual(['correct-audience', 'other-audience'])
562
+ })
563
+
564
+ it('JwtTokenAudience(wrong string - string)', async () => {
565
+ const tok =
566
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoid3JvbmctYXVkaWVuY2UiLCJpYXQiOjF9.2vTYLiYL5r6qN-iRQ0VSfXh4ioLFtNzo0qc-OoPZmow'
567
+ const secret = 'a-secret'
568
+ let err
569
+ let authorized
570
+ try {
571
+ authorized = await JWT.verify(tok, secret, {
572
+ alg: AlgorithmTypes.HS256,
573
+ aud: 'correct-audience',
574
+ })
575
+ } catch (e) {
576
+ err = e
577
+ }
578
+ expect(err).toEqual(new JwtTokenAudience('correct-audience', 'wrong-audience'))
579
+ expect(authorized).toBeUndefined()
580
+ })
581
+
582
+ it('JwtTokenAudience(wrong string - string[])', async () => {
583
+ const tok =
584
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoid3JvbmctYXVkaWVuY2UiLCJpYXQiOjF9.2vTYLiYL5r6qN-iRQ0VSfXh4ioLFtNzo0qc-OoPZmow'
585
+ const secret = 'a-secret'
586
+ let err
587
+ let authorized
588
+ try {
589
+ authorized = await JWT.verify(tok, secret, {
590
+ alg: AlgorithmTypes.HS256,
591
+ aud: ['correct-audience', 'other-audience'],
592
+ })
593
+ } catch (e) {
594
+ err = e
595
+ }
596
+ expect(err).toEqual(
597
+ new JwtTokenAudience(['correct-audience', 'other-audience'], 'wrong-audience')
598
+ )
599
+ expect(authorized).toBeUndefined()
600
+ })
601
+
602
+ it('JwtTokenAudience(wrong string - RegExp)', async () => {
603
+ const tok =
604
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoid3JvbmctYXVkaWVuY2UiLCJpYXQiOjF9.2vTYLiYL5r6qN-iRQ0VSfXh4ioLFtNzo0qc-OoPZmow'
605
+ const secret = 'a-secret'
606
+ let err
607
+ let authorized
608
+ try {
609
+ authorized = await JWT.verify(tok, secret, {
610
+ alg: AlgorithmTypes.HS256,
611
+ aud: /^correct-audience$/,
612
+ })
613
+ } catch (e) {
614
+ err = e
615
+ }
616
+ expect(err).toEqual(new JwtTokenAudience(/^correct-audience$/, 'wrong-audience'))
617
+ expect(authorized).toBeUndefined()
618
+ })
619
+
620
+ it('JwtTokenAudience(wrong string[] - string)', async () => {
621
+ const tok =
622
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbIndyb25nLWF1ZGllbmNlIiwib3RoZXItYXVkaWVuY2UiXSwiaWF0IjoxfQ.YTAM1xtKP4AeEeQSFQ81rcJM1leW_uDayQcTE6LxoP0'
623
+ const secret = 'a-secret'
624
+ let err
625
+ let authorized
626
+ try {
627
+ authorized = await JWT.verify(tok, secret, {
628
+ alg: AlgorithmTypes.HS256,
629
+ aud: 'correct-audience',
630
+ })
631
+ } catch (e) {
632
+ err = e
633
+ }
634
+ expect(err).toEqual(
635
+ new JwtTokenAudience('correct-audience', ['wrong-audience', 'other-audience'])
636
+ )
637
+ expect(authorized).toBeUndefined()
638
+ })
639
+
640
+ it('JwtTokenAudience(wrong string[] - string[])', async () => {
641
+ const tok =
642
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbIndyb25nLWF1ZGllbmNlIiwib3RoZXItYXVkaWVuY2UiXSwiaWF0IjoxfQ.YTAM1xtKP4AeEeQSFQ81rcJM1leW_uDayQcTE6LxoP0'
643
+ const secret = 'a-secret'
644
+ let err
645
+ let authorized
646
+ try {
647
+ authorized = await JWT.verify(tok, secret, {
648
+ alg: AlgorithmTypes.HS256,
649
+ aud: ['correct-audience', 'test'],
650
+ })
651
+ } catch (e) {
652
+ err = e
653
+ }
654
+ expect(err).toEqual(
655
+ new JwtTokenAudience(['correct-audience', 'test'], ['wrong-audience', 'other-audience'])
656
+ )
657
+ expect(authorized).toBeUndefined()
658
+ })
659
+
660
+ it('JwtTokenAudience(wrong string[] - RegExp)', async () => {
661
+ const tok =
662
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjpbIndyb25nLWF1ZGllbmNlIiwib3RoZXItYXVkaWVuY2UiXSwiaWF0IjoxfQ.YTAM1xtKP4AeEeQSFQ81rcJM1leW_uDayQcTE6LxoP0'
663
+ const secret = 'a-secret'
664
+ let err
665
+ let authorized
666
+ try {
667
+ authorized = await JWT.verify(tok, secret, {
668
+ alg: AlgorithmTypes.HS256,
669
+ aud: /^correct-audience$/,
670
+ })
671
+ } catch (e) {
672
+ err = e
673
+ }
674
+ expect(err).toEqual(
675
+ new JwtTokenAudience(/^correct-audience$/, ['wrong-audience', 'other-audience'])
676
+ )
677
+ expect(authorized).toBeUndefined()
678
+ })
679
+
680
+ it('JwtTokenAudience (no aud option and wrong aud in payload)', async () => {
681
+ const tok =
682
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwiYXVkIjoid3JvbmctYXVkaWVuY2UiLCJpYXQiOjF9.2vTYLiYL5r6qN-iRQ0VSfXh4ioLFtNzo0qc-OoPZmow'
683
+ const secret = 'a-secret'
684
+ let err
685
+ let authorized
686
+ try {
687
+ authorized = await JWT.verify(tok, secret, {
688
+ alg: AlgorithmTypes.HS256,
689
+ })
690
+ } catch (e) {
691
+ err = e
692
+ }
693
+ expect(err).toBeUndefined()
694
+ expect(authorized?.aud).toEqual('wrong-audience')
695
+ })
696
+
697
+ it('HS256 sign & verify & decode', async () => {
698
+ const payload = { message: 'hello world' }
699
+ const secret = 'a-secret'
700
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
701
+ const expected =
702
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
703
+ expect(tok).toEqual(expected)
704
+
705
+ const verifiedPayload = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
706
+ expect(verifiedPayload).not.toBeUndefined()
707
+ expect(verifiedPayload).toEqual(payload)
708
+
709
+ expect(JWT.decode(tok)).toEqual({
710
+ header: {
711
+ alg: 'HS256',
712
+ typ: 'JWT',
713
+ },
714
+ payload: {
715
+ message: 'hello world',
716
+ },
717
+ })
718
+ })
719
+
720
+ it('HS256 sign & verify', async () => {
721
+ const payload = { message: 'hello world' }
722
+ const secret = 'a-secret'
723
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
724
+ const expected =
725
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
726
+ expect(tok).toEqual(expected)
727
+
728
+ let err = null
729
+ let authorized
730
+ try {
731
+ authorized = await JWT.verify(tok, secret + 'invalid', AlgorithmTypes.HS256)
732
+ } catch (e) {
733
+ err = e
734
+ }
735
+ expect(authorized).toBeUndefined()
736
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
737
+ })
738
+
739
+ it('HS512 sign & verify & decode', async () => {
740
+ const payload = { message: 'hello world' }
741
+ const secret = 'a-secret'
742
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS512)
743
+ const expected =
744
+ 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.RqVLgExB_GXF1-9T-k4V4HjFmiuQKTEjVSiZd-YL0WERIlywZ7PfzAuTZSJU4gg8cscGamQa030cieEWrYcywg'
745
+ expect(tok).toEqual(expected)
746
+
747
+ const verifiedPayload = await JWT.verify(tok, secret, AlgorithmTypes.HS512)
748
+ expect(verifiedPayload).not.toBeUndefined()
749
+ expect(verifiedPayload).toEqual(payload)
750
+
751
+ expect(JWT.decode(tok)).toEqual({
752
+ header: {
753
+ alg: 'HS512',
754
+ typ: 'JWT',
755
+ },
756
+ payload: {
757
+ message: 'hello world',
758
+ },
759
+ })
760
+ })
761
+
762
+ it('HS512 sign & verify', async () => {
763
+ const payload = { message: 'hello world' }
764
+ const secret = 'a-secret'
765
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS512)
766
+ const expected =
767
+ 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.RqVLgExB_GXF1-9T-k4V4HjFmiuQKTEjVSiZd-YL0WERIlywZ7PfzAuTZSJU4gg8cscGamQa030cieEWrYcywg'
768
+ expect(tok).toEqual(expected)
769
+
770
+ let err = null
771
+ let authorized
772
+ try {
773
+ authorized = await JWT.verify(tok, secret + 'invalid', AlgorithmTypes.HS512)
774
+ } catch (e) {
775
+ err = e
776
+ }
777
+ expect(authorized).toBeUndefined()
778
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
779
+ })
780
+
781
+ it('HS384 sign & verify', async () => {
782
+ const payload = { message: 'hello world' }
783
+ const secret = 'a-secret%你好'
784
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS384)
785
+ const expected =
786
+ 'eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.C1Br1183Oy6O7th4NDCOaI9WB75i3FMCuYlv1tCL9HggsU89T-SNutghwhJykD3r'
787
+ expect(tok).toEqual(expected)
788
+
789
+ let err = null
790
+ let authorized
791
+ try {
792
+ authorized = await JWT.verify(tok, secret + 'invalid', AlgorithmTypes.HS384)
793
+ } catch (e) {
794
+ err = e
795
+ }
796
+ expect(authorized).toBeUndefined()
797
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
798
+ })
799
+
800
+ it('sign & verify & decode with a custom secret', async () => {
801
+ const payload = { message: 'hello world' }
802
+ const algorithm = {
803
+ name: 'HMAC',
804
+ hash: {
805
+ name: 'SHA-256',
806
+ },
807
+ }
808
+ const secret = await crypto.subtle.importKey(
809
+ 'raw',
810
+ Buffer.from('cefb73234d5fae4bf27662900732b52943e8d53e871fe0f353da95de4599c21d', 'hex'),
811
+ algorithm,
812
+ false,
813
+ ['sign', 'verify']
814
+ )
815
+ const tok = await JWT.sign(payload, secret)
816
+ const expected =
817
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.qunGhchNXH_unqWXN6hB0Elhzr5SykSXVhklLti1aFI'
818
+ expect(tok).toEqual(expected)
819
+
820
+ const verifiedPayload = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
821
+ expect(verifiedPayload).not.toBeUndefined()
822
+ expect(verifiedPayload).toEqual(payload)
823
+
824
+ const invalidSecret = await crypto.subtle.importKey(
825
+ 'raw',
826
+ Buffer.from('cefb73234d5fae4bf27662900732b52943e8d53e871fe0f353da95de41111111', 'hex'),
827
+ algorithm,
828
+ false,
829
+ ['sign', 'verify']
830
+ )
831
+ let err = null
832
+ let authorized
833
+ try {
834
+ authorized = await JWT.verify(tok, invalidSecret, AlgorithmTypes.HS256)
835
+ } catch (e) {
836
+ err = e
837
+ }
838
+ expect(authorized).toBeUndefined()
839
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
840
+ })
841
+
842
+ const rsTestCases = [
843
+ {
844
+ alg: AlgorithmTypes.RS256,
845
+ hash: 'SHA-256',
846
+ },
847
+ {
848
+ alg: AlgorithmTypes.RS384,
849
+ hash: 'SHA-384',
850
+ },
851
+ {
852
+ alg: AlgorithmTypes.RS512,
853
+ hash: 'SHA-512',
854
+ },
855
+ ]
856
+ for (const tc of rsTestCases) {
857
+ it(`${tc.alg} sign & verify`, async () => {
858
+ const alg = tc.alg
859
+ const payload = { message: 'hello world' }
860
+ const keyPair = await generateRSAKey(tc.hash)
861
+ const pemPrivateKey = await exportPEMPrivateKey(keyPair.privateKey)
862
+ const pemPublicKey = await exportPEMPublicKey(keyPair.publicKey)
863
+ const jwkPublicKey = await exportJWK(keyPair.publicKey)
864
+
865
+ const tok = await JWT.sign(payload, pemPrivateKey, alg)
866
+ expect(await JWT.verify(tok, pemPublicKey, alg)).toEqual(payload)
867
+ expect(await JWT.verify(tok, pemPrivateKey, alg)).toEqual(payload)
868
+ expect(await JWT.verify(tok, jwkPublicKey, alg)).toEqual(payload)
869
+
870
+ const keyPair2 = await generateRSAKey(tc.hash)
871
+ const unexpectedPemPublicKey = await exportPEMPublicKey(keyPair2.publicKey)
872
+
873
+ let err = null
874
+ let authorized
875
+ try {
876
+ authorized = await JWT.verify(tok, unexpectedPemPublicKey, alg)
877
+ } catch (e) {
878
+ err = e
879
+ }
880
+ expect(authorized).toBeUndefined()
881
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
882
+ })
883
+
884
+ it(`${tc.alg} sign & verify w/ CryptoKey`, async () => {
885
+ const alg = tc.alg
886
+ const payload = { message: 'hello world' }
887
+ const keyPair = await generateRSAKey(tc.hash)
888
+
889
+ const tok = await JWT.sign(payload, keyPair.privateKey, alg)
890
+ expect(await JWT.verify(tok, keyPair.privateKey, alg)).toEqual(payload)
891
+ expect(await JWT.verify(tok, keyPair.publicKey, alg)).toEqual(payload)
892
+ })
893
+ }
894
+
895
+ const psTestCases = [
896
+ {
897
+ alg: AlgorithmTypes.PS256,
898
+ hash: 'SHA-256',
899
+ },
900
+ {
901
+ alg: AlgorithmTypes.PS384,
902
+ hash: 'SHA-384',
903
+ },
904
+ {
905
+ alg: AlgorithmTypes.PS512,
906
+ hash: 'SHA-512',
907
+ },
908
+ ]
909
+ for (const tc of psTestCases) {
910
+ it(`${tc.alg} sign & verify`, async () => {
911
+ const alg = tc.alg
912
+ const payload = { message: 'hello world' }
913
+ const keyPair = await generateRSAPSSKey(tc.hash)
914
+ const pemPrivateKey = await exportPEMPrivateKey(keyPair.privateKey)
915
+ const pemPublicKey = await exportPEMPublicKey(keyPair.publicKey)
916
+ const jwkPublicKey = await exportJWK(keyPair.publicKey)
917
+
918
+ const tok = await JWT.sign(payload, pemPrivateKey, alg)
919
+ expect(await JWT.verify(tok, pemPublicKey, alg)).toEqual(payload)
920
+ expect(await JWT.verify(tok, pemPrivateKey, alg)).toEqual(payload)
921
+ expect(await JWT.verify(tok, jwkPublicKey, alg)).toEqual(payload)
922
+
923
+ const keyPair2 = await generateRSAPSSKey(tc.hash)
924
+ const unexpectedPemPublicKey = await exportPEMPublicKey(keyPair2.publicKey)
925
+
926
+ let err = null
927
+ let authorized
928
+ try {
929
+ authorized = await JWT.verify(tok, unexpectedPemPublicKey, alg)
930
+ } catch (e) {
931
+ err = e
932
+ }
933
+ expect(authorized).toBeUndefined()
934
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
935
+ })
936
+
937
+ it(`${tc.alg} sign & verify w/ CryptoKey`, async () => {
938
+ const alg = tc.alg
939
+ const payload = { message: 'hello world' }
940
+ const keyPair = await generateRSAPSSKey(tc.hash)
941
+
942
+ const tok = await JWT.sign(payload, keyPair.privateKey, alg)
943
+ expect(await JWT.verify(tok, keyPair.privateKey, alg)).toEqual(payload)
944
+ expect(await JWT.verify(tok, keyPair.publicKey, alg)).toEqual(payload)
945
+ })
946
+ }
947
+
948
+ const esTestCases = [
949
+ {
950
+ alg: AlgorithmTypes.ES256,
951
+ namedCurve: 'P-256',
952
+ },
953
+ {
954
+ alg: AlgorithmTypes.ES384,
955
+ namedCurve: 'P-384',
956
+ },
957
+ {
958
+ alg: AlgorithmTypes.ES512,
959
+ namedCurve: 'P-521',
960
+ },
961
+ ]
962
+ for (const tc of esTestCases) {
963
+ it(`${tc.alg} sign & verify`, async () => {
964
+ const alg = tc.alg
965
+ const payload = { message: 'hello world' }
966
+ const keyPair = await generateECDSAKey(tc.namedCurve)
967
+ const pemPrivateKey = await exportPEMPrivateKey(keyPair.privateKey)
968
+ const pemPublicKey = await exportPEMPublicKey(keyPair.publicKey)
969
+ const jwkPublicKey = await exportJWK(keyPair.publicKey)
970
+
971
+ const tok = await JWT.sign(payload, pemPrivateKey, alg)
972
+ expect(await JWT.verify(tok, pemPublicKey, alg)).toEqual(payload)
973
+ expect(await JWT.verify(tok, pemPrivateKey, alg)).toEqual(payload)
974
+ expect(await JWT.verify(tok, jwkPublicKey, alg)).toEqual(payload)
975
+
976
+ const keyPair2 = await generateECDSAKey(tc.namedCurve)
977
+ const unexpectedPemPublicKey = await exportPEMPublicKey(keyPair2.publicKey)
978
+
979
+ let err = null
980
+ let authorized
981
+ try {
982
+ authorized = await JWT.verify(tok, unexpectedPemPublicKey, alg)
983
+ } catch (e) {
984
+ err = e
985
+ }
986
+ expect(authorized).toBeUndefined()
987
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
988
+ })
989
+
990
+ it(`${tc.alg} sign & verify w/ CryptoKey`, async () => {
991
+ const alg = tc.alg
992
+ const payload = { message: 'hello world' }
993
+ const keyPair = await generateECDSAKey(tc.namedCurve)
994
+
995
+ const tok = await JWT.sign(payload, keyPair.privateKey, alg)
996
+ expect(await JWT.verify(tok, keyPair.privateKey, alg)).toEqual(payload)
997
+ expect(await JWT.verify(tok, keyPair.publicKey, alg)).toEqual(payload)
998
+ })
999
+ }
1000
+
1001
+ it('EdDSA sign & verify', async () => {
1002
+ const alg = 'EdDSA'
1003
+ const payload = { message: 'hello world' }
1004
+ const keyPair = await generateEd25519Key()
1005
+ const pemPrivateKey = await exportPEMPrivateKey(keyPair.privateKey)
1006
+ const pemPublicKey = await exportPEMPublicKey(keyPair.publicKey)
1007
+ const jwkPublicKey = await exportJWK(keyPair.publicKey)
1008
+
1009
+ const tok = await JWT.sign(payload, pemPrivateKey, alg)
1010
+ expect(await JWT.verify(tok, pemPublicKey, alg)).toEqual(payload)
1011
+ expect(await JWT.verify(tok, pemPrivateKey, alg)).toEqual(payload)
1012
+ expect(await JWT.verify(tok, jwkPublicKey, alg)).toEqual(payload)
1013
+
1014
+ const keyPair2 = await generateEd25519Key()
1015
+ const unexpectedPemPublicKey = await exportPEMPublicKey(keyPair2.publicKey)
1016
+
1017
+ let err = null
1018
+ let authorized
1019
+ try {
1020
+ authorized = await JWT.verify(tok, unexpectedPemPublicKey, alg)
1021
+ } catch (e) {
1022
+ err = e
1023
+ }
1024
+ expect(authorized).toBeUndefined()
1025
+ expect(err instanceof JwtTokenSignatureMismatched).toBe(true)
1026
+ })
1027
+
1028
+ it('EdDSA sign & verify w/ CryptoKey', async () => {
1029
+ const alg = 'EdDSA'
1030
+ const payload = { message: 'hello world' }
1031
+ const keyPair = await generateEd25519Key()
1032
+
1033
+ const tok = await JWT.sign(payload, keyPair.privateKey, alg)
1034
+ expect(await JWT.verify(tok, keyPair.privateKey, alg)).toEqual(payload)
1035
+ expect(await JWT.verify(tok, keyPair.publicKey, alg)).toEqual(payload)
1036
+ })
1037
+ })
1038
+
1039
+ describe('verifyWithJwks header.alg fallback', () => {
1040
+ it('Should use header.alg as fallback when matchingKey.alg is missing', async () => {
1041
+ // Setup: Create a JWT signed with RS256 (asymmetric algorithm)
1042
+ const payload = { message: 'hello world' }
1043
+ const headerAlg = 'RS256'
1044
+ const kid = 'dummy'
1045
+
1046
+ // Generate RSA key pair
1047
+ const keyPair = await crypto.subtle.generateKey(
1048
+ {
1049
+ name: 'RSASSA-PKCS1-v1_5',
1050
+ modulusLength: 2048,
1051
+ publicExponent: new Uint8Array([1, 0, 1]),
1052
+ hash: 'SHA-256',
1053
+ },
1054
+ true,
1055
+ ['sign', 'verify']
1056
+ )
1057
+
1058
+ // Create JWT (signed with RS256)
1059
+ const header = { alg: headerAlg, typ: 'JWT', kid }
1060
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1061
+ const encodedHeader = encode(header)
1062
+ const encodedPayload = encode(payload)
1063
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1064
+
1065
+ // Sign with private key
1066
+ const signatureBuffer = await signing(
1067
+ keyPair.privateKey,
1068
+ headerAlg,
1069
+ utf8Encoder.encode(signingInput)
1070
+ )
1071
+ const signature = encodeBase64Url(signatureBuffer)
1072
+
1073
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1074
+
1075
+ // Export public key as JWK without alg property
1076
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1077
+ const keyWithoutAlg = {
1078
+ ...jwk,
1079
+ kid,
1080
+ use: 'sig',
1081
+ }
1082
+ delete keyWithoutAlg.alg // intentionally omit alg
1083
+
1084
+ const keys = [keyWithoutAlg]
1085
+
1086
+ // Execute: Verify the JWT token signed with RS256 with allowedAlgorithms required
1087
+ const result = await verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })
1088
+
1089
+ // If verification succeeds, it means header.alg was used
1090
+ expect(result).toEqual(payload)
1091
+ })
1092
+ })
1093
+
1094
+ describe('verifyWithJwks security', () => {
1095
+ it('Should reject symmetric algorithm HS256', async () => {
1096
+ const payload = { message: 'hello world' }
1097
+ const secret = 'secret'
1098
+ const kid = 'dummy'
1099
+
1100
+ // Create JWT (signed with HS256)
1101
+ const header = { alg: 'HS256', typ: 'JWT', kid }
1102
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1103
+ const encodedHeader = encode(header)
1104
+ const encodedPayload = encode(payload)
1105
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1106
+
1107
+ const signatureBuffer = await signing(secret, 'HS256', utf8Encoder.encode(signingInput))
1108
+ const signature = encodeBase64Url(signatureBuffer)
1109
+
1110
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1111
+
1112
+ const keys = [
1113
+ {
1114
+ kty: 'oct',
1115
+ kid,
1116
+ k: encodeBase64Url(utf8Encoder.encode(secret).buffer),
1117
+ use: 'sig',
1118
+ alg: 'HS256',
1119
+ },
1120
+ ]
1121
+
1122
+ // HS256 is rejected before allowedAlgorithms check (symmetric algorithm rejection)
1123
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })).rejects.toThrow(
1124
+ JwtSymmetricAlgorithmNotAllowed
1125
+ )
1126
+ })
1127
+
1128
+ it('Should reject symmetric algorithm HS384', async () => {
1129
+ const payload = { message: 'hello world' }
1130
+ const secret = 'secret'
1131
+ const kid = 'dummy'
1132
+
1133
+ const header = { alg: 'HS384', typ: 'JWT', kid }
1134
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1135
+ const encodedHeader = encode(header)
1136
+ const encodedPayload = encode(payload)
1137
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1138
+
1139
+ const signatureBuffer = await signing(secret, 'HS384', utf8Encoder.encode(signingInput))
1140
+ const signature = encodeBase64Url(signatureBuffer)
1141
+
1142
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1143
+
1144
+ const keys = [
1145
+ {
1146
+ kty: 'oct',
1147
+ kid,
1148
+ k: encodeBase64Url(utf8Encoder.encode(secret).buffer),
1149
+ use: 'sig',
1150
+ alg: 'HS384',
1151
+ },
1152
+ ]
1153
+
1154
+ // HS384 is rejected before allowedAlgorithms check (symmetric algorithm rejection)
1155
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })).rejects.toThrow(
1156
+ JwtSymmetricAlgorithmNotAllowed
1157
+ )
1158
+ })
1159
+
1160
+ it('Should reject symmetric algorithm HS512', async () => {
1161
+ const payload = { message: 'hello world' }
1162
+ const secret = 'secret'
1163
+ const kid = 'dummy'
1164
+
1165
+ const header = { alg: 'HS512', typ: 'JWT', kid }
1166
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1167
+ const encodedHeader = encode(header)
1168
+ const encodedPayload = encode(payload)
1169
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1170
+
1171
+ const signatureBuffer = await signing(secret, 'HS512', utf8Encoder.encode(signingInput))
1172
+ const signature = encodeBase64Url(signatureBuffer)
1173
+
1174
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1175
+
1176
+ const keys = [
1177
+ {
1178
+ kty: 'oct',
1179
+ kid,
1180
+ k: encodeBase64Url(utf8Encoder.encode(secret).buffer),
1181
+ use: 'sig',
1182
+ alg: 'HS512',
1183
+ },
1184
+ ]
1185
+
1186
+ // HS512 is rejected before allowedAlgorithms check (symmetric algorithm rejection)
1187
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })).rejects.toThrow(
1188
+ JwtSymmetricAlgorithmNotAllowed
1189
+ )
1190
+ })
1191
+
1192
+ it('Should reject algorithm mismatch between JWK and JWT header', async () => {
1193
+ const payload = { message: 'hello world' }
1194
+ const kid = 'dummy'
1195
+
1196
+ // Generate RS256 key pair
1197
+ const keyPair = await crypto.subtle.generateKey(
1198
+ {
1199
+ name: 'RSASSA-PKCS1-v1_5',
1200
+ modulusLength: 2048,
1201
+ publicExponent: new Uint8Array([1, 0, 1]),
1202
+ hash: 'SHA-256',
1203
+ },
1204
+ true,
1205
+ ['sign', 'verify']
1206
+ )
1207
+
1208
+ // Create JWT with RS384 in header (mismatch with RS256 key)
1209
+ const header = { alg: 'RS384', typ: 'JWT', kid }
1210
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1211
+ const encodedHeader = encode(header)
1212
+ const encodedPayload = encode(payload)
1213
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1214
+
1215
+ // Sign with RS256 key (but header says RS384)
1216
+ const signatureBuffer = await signing(
1217
+ keyPair.privateKey,
1218
+ 'RS256',
1219
+ utf8Encoder.encode(signingInput)
1220
+ )
1221
+ const signature = encodeBase64Url(signatureBuffer)
1222
+
1223
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1224
+
1225
+ // Export public key as JWK with RS256 alg
1226
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1227
+ const keyWithAlg = {
1228
+ ...jwk,
1229
+ kid,
1230
+ use: 'sig',
1231
+ alg: 'RS256', // JWK says RS256, but header says RS384
1232
+ }
1233
+
1234
+ const keys = [keyWithAlg]
1235
+
1236
+ // RS384 in header doesn't match RS256 in JWK alg field
1237
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS384'] })).rejects.toThrow(
1238
+ JwtAlgorithmMismatch
1239
+ )
1240
+ })
1241
+
1242
+ it('Should allow asymmetric algorithm RS256 with matching alg', async () => {
1243
+ const payload = { message: 'hello world' }
1244
+ const kid = 'dummy'
1245
+
1246
+ const keyPair = await crypto.subtle.generateKey(
1247
+ {
1248
+ name: 'RSASSA-PKCS1-v1_5',
1249
+ modulusLength: 2048,
1250
+ publicExponent: new Uint8Array([1, 0, 1]),
1251
+ hash: 'SHA-256',
1252
+ },
1253
+ true,
1254
+ ['sign', 'verify']
1255
+ )
1256
+
1257
+ const header = { alg: 'RS256', typ: 'JWT', kid }
1258
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1259
+ const encodedHeader = encode(header)
1260
+ const encodedPayload = encode(payload)
1261
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1262
+
1263
+ const signatureBuffer = await signing(
1264
+ keyPair.privateKey,
1265
+ 'RS256',
1266
+ utf8Encoder.encode(signingInput)
1267
+ )
1268
+ const signature = encodeBase64Url(signatureBuffer)
1269
+
1270
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1271
+
1272
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1273
+ const keyWithAlg = {
1274
+ ...jwk,
1275
+ kid,
1276
+ use: 'sig',
1277
+ alg: 'RS256',
1278
+ }
1279
+
1280
+ const keys = [keyWithAlg]
1281
+
1282
+ const result = await verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })
1283
+ expect(result).toEqual(payload)
1284
+ })
1285
+
1286
+ it('Should reject algorithm confusion attack (HS256 with RSA public key)', async () => {
1287
+ // This test simulates the algorithm confusion attack where an attacker
1288
+ // tries to use HS256 with a public RSA key as the HMAC secret
1289
+ const payload = { message: 'hello world' }
1290
+ const kid = 'dummy'
1291
+
1292
+ // Generate RSA key pair (normally used for RS256)
1293
+ const keyPair = await crypto.subtle.generateKey(
1294
+ {
1295
+ name: 'RSASSA-PKCS1-v1_5',
1296
+ modulusLength: 2048,
1297
+ publicExponent: new Uint8Array([1, 0, 1]),
1298
+ hash: 'SHA-256',
1299
+ },
1300
+ true,
1301
+ ['sign', 'verify']
1302
+ )
1303
+
1304
+ // Attacker creates a JWT with HS256 in header
1305
+ const header = { alg: 'HS256', typ: 'JWT', kid }
1306
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1307
+ const encodedHeader = encode(header)
1308
+ const encodedPayload = encode(payload)
1309
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1310
+
1311
+ // Export public key as JWK (which would be used in a real attack)
1312
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1313
+
1314
+ // Attacker would sign with the public key bytes as HMAC secret
1315
+ // We don't need to actually sign for this test - just verify rejection
1316
+ const signatureBuffer = await signing('fake-secret', 'HS256', utf8Encoder.encode(signingInput))
1317
+ const signature = encodeBase64Url(signatureBuffer)
1318
+
1319
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1320
+
1321
+ // JWK has RS256 alg, but JWT header has HS256 (confusion attack)
1322
+ const keyWithAlg = {
1323
+ ...jwk,
1324
+ kid,
1325
+ use: 'sig',
1326
+ alg: 'RS256',
1327
+ }
1328
+
1329
+ const keys = [keyWithAlg]
1330
+
1331
+ // Should reject because HS256 is a symmetric algorithm (checked before allowedAlgorithms)
1332
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })).rejects.toThrow(
1333
+ JwtSymmetricAlgorithmNotAllowed
1334
+ )
1335
+ })
1336
+ })
1337
+
1338
+ describe('verifyWithJwks algorithm whitelist', () => {
1339
+ it('Should reject algorithm not in whitelist', async () => {
1340
+ const payload = { message: 'hello world' }
1341
+ const kid = 'dummy'
1342
+
1343
+ // Generate RS256 key pair
1344
+ const keyPair = await crypto.subtle.generateKey(
1345
+ {
1346
+ name: 'RSASSA-PKCS1-v1_5',
1347
+ modulusLength: 2048,
1348
+ publicExponent: new Uint8Array([1, 0, 1]),
1349
+ hash: 'SHA-256',
1350
+ },
1351
+ true,
1352
+ ['sign', 'verify']
1353
+ )
1354
+
1355
+ // Create JWT with RS256
1356
+ const header = { alg: 'RS256', typ: 'JWT', kid }
1357
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1358
+ const encodedHeader = encode(header)
1359
+ const encodedPayload = encode(payload)
1360
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1361
+
1362
+ const signatureBuffer = await signing(
1363
+ keyPair.privateKey,
1364
+ 'RS256',
1365
+ utf8Encoder.encode(signingInput)
1366
+ )
1367
+ const signature = encodeBase64Url(signatureBuffer)
1368
+
1369
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1370
+
1371
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1372
+ const keyWithAlg = {
1373
+ ...jwk,
1374
+ kid,
1375
+ use: 'sig',
1376
+ alg: 'RS256',
1377
+ }
1378
+
1379
+ const keys = [keyWithAlg]
1380
+
1381
+ // RS256 is not in the whitelist (only ES256 is allowed)
1382
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['ES256'] })).rejects.toThrow(
1383
+ JwtAlgorithmNotAllowed
1384
+ )
1385
+ })
1386
+
1387
+ it('Should accept algorithm in whitelist', async () => {
1388
+ const payload = { message: 'hello world' }
1389
+ const kid = 'dummy'
1390
+
1391
+ // Generate RS256 key pair
1392
+ const keyPair = await crypto.subtle.generateKey(
1393
+ {
1394
+ name: 'RSASSA-PKCS1-v1_5',
1395
+ modulusLength: 2048,
1396
+ publicExponent: new Uint8Array([1, 0, 1]),
1397
+ hash: 'SHA-256',
1398
+ },
1399
+ true,
1400
+ ['sign', 'verify']
1401
+ )
1402
+
1403
+ // Create JWT with RS256
1404
+ const header = { alg: 'RS256', typ: 'JWT', kid }
1405
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1406
+ const encodedHeader = encode(header)
1407
+ const encodedPayload = encode(payload)
1408
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1409
+
1410
+ const signatureBuffer = await signing(
1411
+ keyPair.privateKey,
1412
+ 'RS256',
1413
+ utf8Encoder.encode(signingInput)
1414
+ )
1415
+ const signature = encodeBase64Url(signatureBuffer)
1416
+
1417
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1418
+
1419
+ const jwk = await crypto.subtle.exportKey('jwk', keyPair.publicKey)
1420
+ const keyWithAlg = {
1421
+ ...jwk,
1422
+ kid,
1423
+ use: 'sig',
1424
+ alg: 'RS256',
1425
+ }
1426
+
1427
+ const keys = [keyWithAlg]
1428
+
1429
+ // RS256 is in the whitelist
1430
+ const result = await verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256', 'ES256'] })
1431
+ expect(result).toEqual(payload)
1432
+ })
1433
+
1434
+ // Note: Tests for "whitelist not specified" and "empty whitelist" were removed
1435
+ // because allowedAlgorithms is now required (not optional).
1436
+ // This is a breaking change that enforces explicit algorithm specification for security.
1437
+
1438
+ it('Should reject symmetric algorithm (HS256) in JWT header', async () => {
1439
+ // This test verifies that symmetric algorithms are rejected even when
1440
+ // using verifyWithJwks with asymmetric algorithm whitelist.
1441
+ // Note: HS256 cannot be added to allowedAlgorithms due to type constraints (AsymmetricAlgorithm[])
1442
+ const payload = { message: 'hello world' }
1443
+ const secret = 'secret'
1444
+ const kid = 'dummy'
1445
+
1446
+ // Create JWT with HS256
1447
+ const header = { alg: 'HS256', typ: 'JWT', kid }
1448
+ const encode = (obj: object) => encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1449
+ const encodedHeader = encode(header)
1450
+ const encodedPayload = encode(payload)
1451
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1452
+
1453
+ const signatureBuffer = await signing(secret, 'HS256', utf8Encoder.encode(signingInput))
1454
+ const signature = encodeBase64Url(signatureBuffer)
1455
+
1456
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1457
+
1458
+ const keys = [
1459
+ {
1460
+ kty: 'oct',
1461
+ kid,
1462
+ k: encodeBase64Url(utf8Encoder.encode(secret).buffer),
1463
+ use: 'sig',
1464
+ alg: 'HS256',
1465
+ },
1466
+ ]
1467
+
1468
+ // HS256 in JWT header should be rejected as symmetric algorithm
1469
+ // (symmetric algorithm check happens before allowedAlgorithms check)
1470
+ await expect(verifyWithJwks(token, { keys, allowedAlgorithms: ['RS256'] })).rejects.toThrow(
1471
+ JwtSymmetricAlgorithmNotAllowed
1472
+ )
1473
+ })
1474
+ })
1475
+
1476
+ describe('verifyWithJwks key handling', () => {
1477
+ it('Should not mutate provided keys when JWKS is fetched repeatedly', async () => {
1478
+ const localKeys: AppJsonWebKey[] = [
1479
+ {
1480
+ kty: 'RSA',
1481
+ kid: 'local-key',
1482
+ alg: 'RS256',
1483
+ e: 'AQAB',
1484
+ n: 'sXchAZo4YqB7f1_g8U9RVcdpShUMHbOWcZHhGXLiCFYI8aAizI0s5momkMumZ5qX6Ch12yvDqOiiMHDLecxB2S7RMyCV2wAPOQgpdnXl16rDpD6PEw24kTx5cDIeEJD7BqXc9Ejo4kKDAdAm8YGtS-wGGyRyvE4s46HoPazTA7k',
1485
+ use: 'sig',
1486
+ },
1487
+ ]
1488
+
1489
+ const originalKeys = structuredClone(localKeys)
1490
+ const originalFetch = globalThis.fetch
1491
+ const header = Buffer.from(
1492
+ JSON.stringify({ alg: 'RS256', typ: 'JWT', kid: 'unknown-key' })
1493
+ ).toString('base64url')
1494
+ const payload = Buffer.from(JSON.stringify({})).toString('base64url')
1495
+ const token = `${header}.${payload}.x`
1496
+
1497
+ try {
1498
+ globalThis.fetch = (async () => {
1499
+ return new Response(
1500
+ JSON.stringify({
1501
+ keys: [
1502
+ { ...localKeys[0], kid: 'remote-key' },
1503
+ { ...localKeys[0], kid: 'remote-key' },
1504
+ ],
1505
+ }),
1506
+ { status: 200, headers: { 'content-type': 'application/json' } }
1507
+ )
1508
+ }) as typeof globalThis.fetch
1509
+
1510
+ await expect(
1511
+ verifyWithJwks(token, {
1512
+ keys: localKeys,
1513
+ jwks_uri: 'https://example.invalid/.well-known/jwks.json',
1514
+ allowedAlgorithms: ['RS256'],
1515
+ })
1516
+ ).rejects.toThrow(JwtTokenInvalid)
1517
+
1518
+ await expect(
1519
+ verifyWithJwks(token, {
1520
+ keys: localKeys,
1521
+ jwks_uri: 'https://example.invalid/.well-known/jwks.json',
1522
+ allowedAlgorithms: ['RS256'],
1523
+ })
1524
+ ).rejects.toThrow(JwtTokenInvalid)
1525
+ } finally {
1526
+ globalThis.fetch = originalFetch
1527
+ }
1528
+
1529
+ expect(localKeys).toEqual(originalKeys)
1530
+ })
1531
+ })
1532
+
1533
+ async function exportPEMPrivateKey(key: CryptoKey): Promise<string> {
1534
+ const exported = await crypto.subtle.exportKey('pkcs8', key)
1535
+ const pem = `-----BEGIN PRIVATE KEY-----\n${encodeBase64(exported)}\n-----END PRIVATE KEY-----`
1536
+ return pem
1537
+ }
1538
+
1539
+ async function exportPEMPublicKey(key: CryptoKey): Promise<string> {
1540
+ const exported = await crypto.subtle.exportKey('spki', key)
1541
+ const pem = `-----BEGIN PUBLIC KEY-----\n${encodeBase64(exported)}\n-----END PUBLIC KEY-----`
1542
+ return pem
1543
+ }
1544
+
1545
+ async function exportJWK(key: CryptoKey): Promise<JsonWebKey> {
1546
+ return await crypto.subtle.exportKey('jwk', key)
1547
+ }
1548
+
1549
+ async function generateRSAKey(hash: string): Promise<CryptoKeyPair> {
1550
+ return await crypto.subtle.generateKey(
1551
+ {
1552
+ hash,
1553
+ modulusLength: 2048,
1554
+ publicExponent: new Uint8Array([1, 0, 1]),
1555
+ name: 'RSASSA-PKCS1-v1_5',
1556
+ },
1557
+ true,
1558
+ ['sign', 'verify']
1559
+ )
1560
+ }
1561
+
1562
+ async function generateRSAPSSKey(hash: string): Promise<CryptoKeyPair> {
1563
+ return await crypto.subtle.generateKey(
1564
+ {
1565
+ hash,
1566
+ modulusLength: 2048,
1567
+ publicExponent: new Uint8Array([1, 0, 1]),
1568
+ name: 'RSA-PSS',
1569
+ },
1570
+ true,
1571
+ ['sign', 'verify']
1572
+ )
1573
+ }
1574
+
1575
+ async function generateECDSAKey(namedCurve: string): Promise<CryptoKeyPair> {
1576
+ return await crypto.subtle.generateKey(
1577
+ {
1578
+ name: 'ECDSA',
1579
+ namedCurve,
1580
+ },
1581
+ true,
1582
+ ['sign', 'verify']
1583
+ )
1584
+ }
1585
+
1586
+ async function generateEd25519Key(): Promise<CryptoKeyPair> {
1587
+ return await crypto.subtle.generateKey(
1588
+ {
1589
+ name: 'Ed25519',
1590
+ namedCurve: 'Ed25519',
1591
+ },
1592
+ true,
1593
+ ['sign', 'verify']
1594
+ )
1595
+ }
1596
+
1597
+ describe('Security: Algorithm Confusion Attack Prevention', () => {
1598
+ it('Should throw JwtAlgorithmRequired error when alg is not specified in verify()', async () => {
1599
+ const payload = { message: 'hello world' }
1600
+ const secret = 'a-secret'
1601
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
1602
+
1603
+ let err: JwtAlgorithmRequired | undefined
1604
+ let authorized
1605
+ try {
1606
+ // @ts-expect-error - intentionally testing without alg parameter
1607
+ authorized = await JWT.verify(tok, secret)
1608
+ } catch (e) {
1609
+ err = e as JwtAlgorithmRequired
1610
+ }
1611
+ expect(err).toBeInstanceOf(JwtAlgorithmRequired)
1612
+ expect(err?.message).toBe('JWT verification requires "alg" option to be specified')
1613
+ expect(authorized).toBeUndefined()
1614
+ })
1615
+
1616
+ it('Should throw JwtAlgorithmRequired error when alg is undefined in options object', async () => {
1617
+ const payload = { message: 'hello world' }
1618
+ const secret = 'a-secret'
1619
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
1620
+
1621
+ let err: JwtAlgorithmRequired | undefined
1622
+ let authorized
1623
+ try {
1624
+ // @ts-expect-error - intentionally testing with undefined alg
1625
+ authorized = await JWT.verify(tok, secret, { alg: undefined })
1626
+ } catch (e) {
1627
+ err = e as JwtAlgorithmRequired
1628
+ }
1629
+ expect(err).toBeInstanceOf(JwtAlgorithmRequired)
1630
+ expect(authorized).toBeUndefined()
1631
+ })
1632
+
1633
+ it('Should prevent algorithm confusion attack (RS256 token verified with HS256 using public key)', async () => {
1634
+ // Generate RSA key pair
1635
+ const keyPair = await crypto.subtle.generateKey(
1636
+ {
1637
+ name: 'RSASSA-PKCS1-v1_5',
1638
+ modulusLength: 2048,
1639
+ publicExponent: new Uint8Array([1, 0, 1]),
1640
+ hash: 'SHA-256',
1641
+ },
1642
+ true,
1643
+ ['sign', 'verify']
1644
+ )
1645
+
1646
+ const payload = { message: 'hello world' }
1647
+
1648
+ // Sign token with RS256 using private key
1649
+ const tok = await JWT.sign(payload, keyPair.privateKey, AlgorithmTypes.RS256)
1650
+
1651
+ // Verify should succeed with RS256 and public key
1652
+ const verified = await JWT.verify(tok, keyPair.publicKey, AlgorithmTypes.RS256)
1653
+ expect(verified).toEqual(payload)
1654
+
1655
+ // Attempting to verify with HS256 using the public key should fail with algorithm mismatch
1656
+ // This simulates the algorithm confusion attack scenario
1657
+ let err: Error | undefined
1658
+ let maliciousResult
1659
+ try {
1660
+ maliciousResult = await JWT.verify(tok, keyPair.publicKey, AlgorithmTypes.HS256)
1661
+ } catch (e) {
1662
+ err = e as Error
1663
+ }
1664
+ // The verification should fail because the header.alg (RS256) doesn't match options.alg (HS256)
1665
+ expect(maliciousResult).toBeUndefined()
1666
+ expect(err).toBeInstanceOf(JwtAlgorithmMismatch)
1667
+ })
1668
+
1669
+ it('Should throw JwtAlgorithmMismatch when header.alg does not match options.alg', async () => {
1670
+ const payload = { message: 'hello world' }
1671
+ const secret = 'a-secret'
1672
+
1673
+ // Sign with HS512
1674
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS512)
1675
+
1676
+ // Try to verify with HS256 (wrong algorithm)
1677
+ let err: Error | undefined
1678
+ let result
1679
+ try {
1680
+ result = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
1681
+ } catch (e) {
1682
+ err = e as Error
1683
+ }
1684
+
1685
+ expect(result).toBeUndefined()
1686
+ expect(err).toBeInstanceOf(JwtAlgorithmMismatch)
1687
+ })
1688
+
1689
+ it('Should require explicit alg specification to prevent default fallback attack', async () => {
1690
+ const payload = { message: 'hello world' }
1691
+ const secret = 'a-secret'
1692
+
1693
+ // Create a token with HS256
1694
+ const tok = await JWT.sign(payload, secret, AlgorithmTypes.HS256)
1695
+
1696
+ // Verify with explicit HS256 should work
1697
+ const verified = await JWT.verify(tok, secret, AlgorithmTypes.HS256)
1698
+ expect(verified).toEqual(payload)
1699
+
1700
+ // Verify without alg should throw error (no default fallback)
1701
+ let err: Error | undefined
1702
+ try {
1703
+ // @ts-expect-error - intentionally testing without alg parameter
1704
+ await JWT.verify(tok, secret)
1705
+ } catch (e) {
1706
+ err = e as Error
1707
+ }
1708
+ expect(err).toBeInstanceOf(JwtAlgorithmRequired)
1709
+ })
1710
+ })
1711
+
1712
+ describe('JWT decode token format validation', () => {
1713
+ it('decode should throw JwtTokenInvalid for token with 2 parts', () => {
1714
+ const malformed = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8ifQ'
1715
+ expect(() => JWT.decode(malformed)).toThrow(JwtTokenInvalid)
1716
+ })
1717
+
1718
+ it('decode should throw JwtTokenInvalid for token with 1 part', () => {
1719
+ expect(() => JWT.decode('eyJhbGciOiJIUzI1NiJ9')).toThrow(JwtTokenInvalid)
1720
+ })
1721
+
1722
+ it('decode should throw JwtTokenInvalid for token with 4 parts', () => {
1723
+ const fourParts = 'a.b.c.d'
1724
+ expect(() => JWT.decode(fourParts)).toThrow(JwtTokenInvalid)
1725
+ })
1726
+
1727
+ it('decode should throw JwtTokenInvalid for empty string', () => {
1728
+ expect(() => JWT.decode('')).toThrow(JwtTokenInvalid)
1729
+ })
1730
+
1731
+ it('decodeHeader should throw JwtTokenInvalid for token with 2 parts', () => {
1732
+ const malformed = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8ifQ'
1733
+ expect(() => JWT.decodeHeader(malformed)).toThrow(JwtTokenInvalid)
1734
+ })
1735
+
1736
+ it('decodeHeader should throw JwtTokenInvalid for empty string', () => {
1737
+ expect(() => JWT.decodeHeader('')).toThrow(JwtTokenInvalid)
1738
+ })
1739
+
1740
+ it('decode should work for valid 3-part token', async () => {
1741
+ const secret = 'a-secret'
1742
+ const tok = await JWT.sign({ message: 'hello' }, secret, AlgorithmTypes.HS256)
1743
+ const decoded = JWT.decode(tok)
1744
+ expect(decoded.header.alg).toBe('HS256')
1745
+ expect(decoded.payload).toEqual({ message: 'hello' })
1746
+ })
1747
+
1748
+ it('decodeHeader should work for valid 3-part token', async () => {
1749
+ const secret = 'a-secret'
1750
+ const tok = await JWT.sign({ message: 'hello' }, secret, AlgorithmTypes.HS256)
1751
+ const header = JWT.decodeHeader(tok)
1752
+ expect(header.alg).toBe('HS256')
1753
+ expect(header.typ).toBe('JWT')
1754
+ })
1755
+ })
1756
+
1757
+ describe('PEM parsing', async () => {
1758
+ const keyPair = await crypto.subtle.generateKey(
1759
+ {
1760
+ name: 'Ed25519',
1761
+ },
1762
+ true,
1763
+ ['sign', 'verify']
1764
+ )
1765
+
1766
+ const exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
1767
+ const base64Key = Buffer.from(exported).toString('base64')
1768
+ const privateKey = `-----BEGIN PRIVATE KEY-----\n${base64Key}\n-----END PRIVATE KEY-----`
1769
+
1770
+ it('should sign & verify with a normal private PEM key', async () => {
1771
+ const payload = { sub: '123' }
1772
+
1773
+ const token = await JWT.sign(payload, privateKey, 'EdDSA')
1774
+ const verified = await JWT.verify(token, privateKey, 'EdDSA')
1775
+
1776
+ expect(verified).toEqual(payload)
1777
+ })
1778
+
1779
+ it('should sign & verify with a single-line private PEM key', async () => {
1780
+ const singleLinePrivateKey = privateKey.replace(/\n/g, '')
1781
+ const payload = { sub: '123' }
1782
+
1783
+ const token = await JWT.sign(payload, singleLinePrivateKey, 'EdDSA')
1784
+ const verified = await JWT.verify(token, privateKey, 'EdDSA')
1785
+
1786
+ expect(verified).toEqual(payload)
1787
+ })
1788
+ })