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,1135 @@
1
+ import { HttpResponse, http } from 'msw'
2
+ import { setupServer } from 'msw/node'
3
+ import { jwk } from './index.ts'
4
+ import { setSignedCookie } from '../../helper/cookie/index.ts'
5
+ import { App } from '../../app.ts'
6
+ import { HTTPException } from '../../http-exception.ts'
7
+ import { encodeBase64Url } from '../../utils/encode.ts'
8
+ import { Jwt } from '../../utils/jwt/index.ts'
9
+ import type { AppJsonWebKey } from '../../utils/jwt/jws.ts'
10
+ import { signing } from '../../utils/jwt/jws.ts'
11
+ import { verifyWithJwks } from '../../utils/jwt/jwt.ts'
12
+ import type { JWTPayload } from '../../utils/jwt/types.ts'
13
+ import { utf8Encoder } from '../../utils/jwt/utf8.ts'
14
+ import test_keys from './keys.test.json' with { type: 'json' }
15
+
16
+ const verify_keys = test_keys.public_keys
17
+
18
+ describe('JWK', () => {
19
+ const server = setupServer(
20
+ http.get('http://localhost/.well-known/jwks.json', () => {
21
+ return HttpResponse.json({ keys: verify_keys })
22
+ }),
23
+ http.get('http://localhost/.well-known/missing-jwks.json', () => {
24
+ return HttpResponse.json({})
25
+ }),
26
+ http.get('http://localhost/.well-known/bad-jwks.json', () => {
27
+ return HttpResponse.json({ keys: 'bad-keys' })
28
+ }),
29
+ http.get('http://localhost/.well-known/404-jwks.json', () => {
30
+ return HttpResponse.text('Not Found', { status: 404 })
31
+ })
32
+ )
33
+ beforeAll(() => server.listen())
34
+ afterEach(() => server.resetHandlers())
35
+ afterAll(() => server.close())
36
+
37
+ describe('verifyWithJwks', () => {
38
+ it('Should throw error on missing keys/jwks_uri options', async () => {
39
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
40
+ await expect(verifyWithJwks(credential, { allowedAlgorithms: ['RS256'] })).rejects.toThrow(
41
+ 'verifyWithJwks requires options for either "keys" or "jwks_uri" or both'
42
+ )
43
+ })
44
+ })
45
+
46
+ describe('options.allow_anon = true', () => {
47
+ let handlerExecuted: boolean
48
+
49
+ beforeEach(() => {
50
+ handlerExecuted = false
51
+ })
52
+
53
+ const app = new App()
54
+
55
+ app.use('/backend-auth-or-anon/*', jwk({ keys: verify_keys, allow_anon: true, alg: ['RS256'] }))
56
+
57
+ app.get('/backend-auth-or-anon/*', (c) => {
58
+ handlerExecuted = true
59
+ const payload = c.get('jwtPayload')
60
+ return c.json(payload ?? { message: 'hello anon' })
61
+ })
62
+
63
+ it('Should skip JWK if no token is present', async () => {
64
+ const req = new Request('http://localhost/backend-auth-or-anon/a')
65
+ const res = await app.request(req)
66
+ expect(res).not.toBeNull()
67
+ expect(res.status).toBe(200)
68
+ expect(await res.json()).toEqual({ message: 'hello anon' })
69
+ expect(handlerExecuted).toBeTruthy()
70
+ })
71
+
72
+ it('Should authorize if token is present', async () => {
73
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
74
+ const req = new Request('http://localhost/backend-auth-or-anon/a')
75
+ req.headers.set('Authorization', `Bearer ${credential}`)
76
+ const res = await app.request(req)
77
+ expect(res).not.toBeNull()
78
+ expect(res.status).toBe(200)
79
+ expect(await res.json()).toEqual({ message: 'hello world' })
80
+ expect(handlerExecuted).toBeTruthy()
81
+ })
82
+
83
+ it('Should not authorize if a non-Bearer scheme is present', async () => {
84
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
85
+ const url = 'http://localhost/backend-auth-or-anon/a'
86
+ const req = new Request(url)
87
+ req.headers.set('Authorization', `Basic ${credential}`)
88
+ const res = await app.request(req)
89
+ expect(res).not.toBeNull()
90
+ expect(res.status).toBe(401)
91
+ expect(res.headers.get('www-authenticate')).toEqual(
92
+ `Bearer realm="${url}",error="invalid_request",error_description="invalid credentials structure"`
93
+ )
94
+ expect(handlerExecuted).toBeFalsy()
95
+ })
96
+
97
+ it('Should not authorize if an invalid Bearer token is present', async () => {
98
+ const invalidToken =
99
+ 'ssyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
100
+ const url = 'http://localhost/backend-auth-or-anon/a'
101
+ const req = new Request(url)
102
+ req.headers.set('Authorization', `Bearer ${invalidToken}`)
103
+ const res = await app.request(req)
104
+ expect(res).not.toBeNull()
105
+ expect(res.status).toBe(401)
106
+ expect(res.headers.get('www-authenticate')).toEqual(
107
+ `Bearer realm="${url}",error="invalid_token",error_description="token verification failure"`
108
+ )
109
+ expect(handlerExecuted).toBeFalsy()
110
+ })
111
+ })
112
+
113
+ describe('Credentials in header', () => {
114
+ let handlerExecuted: boolean
115
+
116
+ beforeEach(() => {
117
+ handlerExecuted = false
118
+ })
119
+
120
+ const app = new App()
121
+
122
+ app.use('/auth-with-keys/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
123
+ app.use('/auth-with-keys-unicode/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
124
+ app.use('/auth-with-keys-nested/*', async (c, next) => {
125
+ const auth = jwk({ keys: verify_keys, alg: ['RS256'] })
126
+ return auth(c, next)
127
+ })
128
+ app.use(
129
+ '/auth-with-keys-fn/*',
130
+ jwk({
131
+ keys: async () => {
132
+ const response = await fetch('http://localhost/.well-known/jwks.json')
133
+ const data = await response.json()
134
+ return data.keys
135
+ },
136
+ alg: ['RS256'],
137
+ })
138
+ )
139
+ app.use(
140
+ '/auth-with-jwks_uri/*',
141
+ jwk({
142
+ jwks_uri: 'http://localhost/.well-known/jwks.json',
143
+ alg: ['RS256'],
144
+ })
145
+ )
146
+ app.use(
147
+ '/auth-with-keys-and-jwks_uri/*',
148
+ jwk({
149
+ keys: verify_keys,
150
+ jwks_uri: () => 'http://localhost/.well-known/jwks.json',
151
+ alg: ['RS256'],
152
+ })
153
+ )
154
+ app.use(
155
+ '/auth-with-missing-jwks_uri/*',
156
+ jwk({
157
+ jwks_uri: 'http://localhost/.well-known/missing-jwks.json',
158
+ alg: ['RS256'],
159
+ })
160
+ )
161
+ app.use(
162
+ '/auth-with-404-jwks_uri/*',
163
+ jwk({
164
+ jwks_uri: 'http://localhost/.well-known/404-jwks.json',
165
+ alg: ['RS256'],
166
+ })
167
+ )
168
+ app.use(
169
+ '/auth-with-bad-jwks_uri/*',
170
+ jwk({
171
+ jwks_uri: 'http://localhost/.well-known/bad-jwks.json',
172
+ alg: ['RS256'],
173
+ })
174
+ )
175
+
176
+ app.get('/auth-with-keys/*', (c) => {
177
+ handlerExecuted = true
178
+ const payload = c.get('jwtPayload')
179
+ return c.json(payload)
180
+ })
181
+ app.get('/auth-with-keys-unicode/*', (c) => {
182
+ handlerExecuted = true
183
+ const payload = c.get('jwtPayload')
184
+ return c.json(payload)
185
+ })
186
+ app.get('/auth-with-keys-nested/*', (c) => {
187
+ handlerExecuted = true
188
+ const payload = c.get('jwtPayload')
189
+ return c.json(payload)
190
+ })
191
+ app.get('/auth-with-keys-fn/*', (c) => {
192
+ handlerExecuted = true
193
+ const payload = c.get('jwtPayload')
194
+ return c.json(payload)
195
+ })
196
+ app.get('/auth-with-jwks_uri/*', (c) => {
197
+ handlerExecuted = true
198
+ const payload = c.get('jwtPayload')
199
+ return c.json(payload)
200
+ })
201
+ app.get('/auth-with-keys-and-jwks_uri/*', (c) => {
202
+ handlerExecuted = true
203
+ const payload = c.get('jwtPayload')
204
+ return c.json(payload)
205
+ })
206
+ app.get('/auth-with-missing-jwks_uri/*', (c) => {
207
+ handlerExecuted = true
208
+ const payload = c.get('jwtPayload')
209
+ return c.json(payload)
210
+ })
211
+ app.get('/auth-with-404-jwks_uri/*', (c) => {
212
+ handlerExecuted = true
213
+ const payload = c.get('jwtPayload')
214
+ return c.json(payload)
215
+ })
216
+ app.get('/auth-with-bad-jwks_uri/*', (c) => {
217
+ handlerExecuted = true
218
+ const payload = c.get('jwtPayload')
219
+ return c.json(payload)
220
+ })
221
+
222
+ it('Should throw an error if the middleware is missing both keys and jwks_uri (empty)', async () => {
223
+ // @ts-expect-error - Testing runtime error with missing required alg option
224
+ expect(() => app.use('/auth-with-empty-middleware/*', jwk({}))).toThrow(
225
+ 'JWK auth middleware requires options for either "keys" or "jwks_uri"'
226
+ )
227
+ })
228
+
229
+ it('Should throw an error when crypto.subtle is missing', async () => {
230
+ const subtleSpy = vi.spyOn(global.crypto, 'subtle', 'get').mockReturnValue({
231
+ importKey: undefined,
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ } as any)
234
+ expect(() =>
235
+ app.use('/auth-with-bad-env/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
236
+ ).toThrow()
237
+ subtleSpy.mockRestore()
238
+ })
239
+
240
+ it('Should return a server error if options.jwks_uri returns a 404', async () => {
241
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
242
+ const req = new Request('http://localhost/auth-with-404-jwks_uri/a')
243
+ req.headers.set('Authorization', `Bearer ${credential}`)
244
+ const res = await app.request(req)
245
+ expect(res).not.toBeNull()
246
+ expect(res.status).toBe(500)
247
+ })
248
+
249
+ it('Should return a server error if the remotely fetched keys from options.jwks_uri are missing', async () => {
250
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
251
+ const req = new Request('http://localhost/auth-with-missing-jwks_uri/a')
252
+ req.headers.set('Authorization', `Bearer ${credential}`)
253
+ const res = await app.request(req)
254
+ expect(res).not.toBeNull()
255
+ expect(res.status).toBe(500)
256
+ })
257
+
258
+ it('Should return a server error if the remotely fetched keys from options.jwks_uri are malformed', async () => {
259
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
260
+ const req = new Request('http://localhost/auth-with-bad-jwks_uri/a')
261
+ req.headers.set('Authorization', `Bearer ${credential}`)
262
+ const res = await app.request(req)
263
+ expect(res).not.toBeNull()
264
+ expect(res.status).toBe(500)
265
+ })
266
+
267
+ it('Should not authorize requests with missing access token', async () => {
268
+ const req = new Request('http://localhost/auth-with-keys/a')
269
+ const res = await app.request(req)
270
+ expect(res).not.toBeNull()
271
+ expect(res.status).toBe(401)
272
+ expect(await res.text()).toBe('Unauthorized')
273
+ expect(handlerExecuted).toBeFalsy()
274
+ })
275
+
276
+ it('Should authorize from a static array passed to options.keys (key 1)', async () => {
277
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
278
+ const req = new Request('http://localhost/auth-with-keys/a')
279
+ req.headers.set('Authorization', `Bearer ${credential}`)
280
+ const res = await app.request(req)
281
+ expect(res).not.toBeNull()
282
+ expect(res.status).toBe(200)
283
+ expect(await res.json()).toEqual({ message: 'hello world' })
284
+ expect(handlerExecuted).toBeTruthy()
285
+ })
286
+
287
+ it('Should authorize from a static array passed to options.keys (key 2)', async () => {
288
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[1])
289
+ const req = new Request('http://localhost/auth-with-keys/a')
290
+ req.headers.set('Authorization', `Bearer ${credential}`)
291
+ const res = await app.request(req)
292
+ expect(res).not.toBeNull()
293
+ expect(res.status).toBe(200)
294
+ expect(await res.json()).toEqual({ message: 'hello world' })
295
+ expect(handlerExecuted).toBeTruthy()
296
+ expect(res.status).toBe(200)
297
+ })
298
+
299
+ it('Should not authorize a token without header', async () => {
300
+ const encodeJwtPart = (part: unknown): string =>
301
+ encodeBase64Url(utf8Encoder.encode(JSON.stringify(part)).buffer).replace(/=/g, '')
302
+ const encodeSignaturePart = (buf: ArrayBufferLike): string =>
303
+ encodeBase64Url(buf).replace(/=/g, '')
304
+ const jwtSignWithoutHeader = async (payload: JWTPayload, privateKey: AppJsonWebKey) => {
305
+ const encodedPayload = encodeJwtPart(payload)
306
+ const signaturePart = await signing(
307
+ privateKey,
308
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
309
+ privateKey.alg as any,
310
+ utf8Encoder.encode(encodedPayload)
311
+ )
312
+ const signature = encodeSignaturePart(signaturePart)
313
+ return `${encodedPayload}.${signature}`
314
+ }
315
+ const credential = await jwtSignWithoutHeader(
316
+ { message: 'hello world' },
317
+ test_keys.private_keys[1]
318
+ )
319
+ const req = new Request('http://localhost/auth-with-keys/a')
320
+ req.headers.set('Authorization', `Bearer ${credential}`)
321
+ const res = await app.request(req)
322
+ expect(res).not.toBeNull()
323
+ expect(res.status).toBe(401)
324
+ })
325
+
326
+ it('Should not authorize a token with missing "kid" in header', async () => {
327
+ const encodeJwtPart = (part: unknown): string =>
328
+ encodeBase64Url(utf8Encoder.encode(JSON.stringify(part)).buffer).replace(/=/g, '')
329
+ const encodeSignaturePart = (buf: ArrayBufferLike): string =>
330
+ encodeBase64Url(buf).replace(/=/g, '')
331
+ const jwtSignWithoutKid = async (payload: JWTPayload, privateKey: AppJsonWebKey) => {
332
+ const encodedPayload = encodeJwtPart(payload)
333
+ const encodedHeader = encodeJwtPart({ alg: privateKey.alg, typ: 'JWT' })
334
+ const partialToken = `${encodedHeader}.${encodedPayload}`
335
+ const signaturePart = await signing(
336
+ privateKey,
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ privateKey.alg as any,
339
+ utf8Encoder.encode(partialToken)
340
+ )
341
+ const signature = encodeSignaturePart(signaturePart)
342
+ return `${partialToken}.${signature}`
343
+ }
344
+ const credential = await jwtSignWithoutKid(
345
+ { message: 'hello world' },
346
+ test_keys.private_keys[1]
347
+ )
348
+ const req = new Request('http://localhost/auth-with-keys/a')
349
+ req.headers.set('Authorization', `Bearer ${credential}`)
350
+ const res = await app.request(req)
351
+ expect(res).not.toBeNull()
352
+ expect(res.status).toBe(401)
353
+ })
354
+
355
+ it('Should not authorize a token with invalid "kid" in header', async () => {
356
+ const copy = structuredClone(test_keys.private_keys[1])
357
+ copy.kid = 'invalid-kid'
358
+ const credential = await Jwt.sign({ message: 'hello world' }, copy)
359
+ const req = new Request('http://localhost/auth-with-keys/a')
360
+ req.headers.set('Authorization', `Bearer ${credential}`)
361
+ const res = await app.request(req)
362
+ expect(res).not.toBeNull()
363
+ expect(res.status).toBe(401)
364
+ })
365
+
366
+ it('Should authorize with Unicode payload from a static array passed to options.keys', async () => {
367
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
368
+ const req = new Request('http://localhost/auth-with-keys-unicode/a')
369
+ req.headers.set('Authorization', `bearer ${credential}`)
370
+ const res = await app.request(req)
371
+ expect(res).not.toBeNull()
372
+ expect(res.status).toBe(200)
373
+ expect(await res.json()).toEqual({ message: 'hello world' })
374
+ expect(handlerExecuted).toBeTruthy()
375
+ })
376
+
377
+ it('Should authorize with mixed-case Bearer scheme', async () => {
378
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
379
+ const req = new Request('http://localhost/auth-with-keys-unicode/a')
380
+ req.headers.set('Authorization', `bEaReR ${credential}`)
381
+ const res = await app.request(req)
382
+ expect(res).not.toBeNull()
383
+ expect(res.status).toBe(200)
384
+ expect(await res.json()).toEqual({ message: 'hello world' })
385
+ expect(handlerExecuted).toBeTruthy()
386
+ })
387
+
388
+ it('Should authorize from a function passed to options.keys', async () => {
389
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
390
+ const req = new Request('http://localhost/auth-with-keys-fn/a')
391
+ req.headers.set('Authorization', `Bearer ${credential}`)
392
+ const res = await app.request(req)
393
+ expect(res).not.toBeNull()
394
+ expect(res.status).toBe(200)
395
+ expect(await res.json()).toEqual({ message: 'hello world' })
396
+ expect(handlerExecuted).toBeTruthy()
397
+ })
398
+
399
+ it('Should authorize from keys remotely fetched from options.jwks_uri', async () => {
400
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
401
+ const req = new Request('http://localhost/auth-with-jwks_uri/a')
402
+ req.headers.set('Authorization', `Bearer ${credential}`)
403
+ const res = await app.request(req)
404
+ expect(res).not.toBeNull()
405
+ expect(res.status).toBe(200)
406
+ expect(await res.json()).toEqual({ message: 'hello world' })
407
+ expect(handlerExecuted).toBeTruthy()
408
+ })
409
+
410
+ it('Should authorize from keys and hard-coded and remotely fetched from options.jwks_uri', async () => {
411
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
412
+ const req = new Request('http://localhost/auth-with-keys-and-jwks_uri/a')
413
+ req.headers.set('Authorization', `Bearer ${credential}`)
414
+ const res = await app.request(req)
415
+ expect(res).not.toBeNull()
416
+ expect(res.status).toBe(200)
417
+ expect(await res.json()).toEqual({ message: 'hello world' })
418
+ expect(handlerExecuted).toBeTruthy()
419
+ })
420
+
421
+ it('Should not authorize requests with non-Bearer scheme in header', async () => {
422
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
423
+ const url = 'http://localhost/auth-with-keys-unicode/a'
424
+ const req = new Request(url)
425
+ req.headers.set('Authorization', `Basic ${credential}`)
426
+ const res = await app.request(req)
427
+ expect(res).not.toBeNull()
428
+ expect(res.status).toBe(401)
429
+ expect(res.headers.get('www-authenticate')).toEqual(
430
+ `Bearer realm="${url}",error="invalid_request",error_description="invalid credentials structure"`
431
+ )
432
+ expect(handlerExecuted).toBeFalsy()
433
+ })
434
+
435
+ it('Should not authorize requests with invalid token in Bearer header', async () => {
436
+ const invalidToken =
437
+ 'ssyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
438
+
439
+ const url = 'http://localhost/auth-with-keys-unicode/a'
440
+ const req = new Request(url)
441
+ req.headers.set('Authorization', `Bearer ${invalidToken}`)
442
+ const res = await app.request(req)
443
+ expect(res).not.toBeNull()
444
+ expect(res.status).toBe(401)
445
+ expect(res.headers.get('www-authenticate')).toEqual(
446
+ `Bearer realm="${url}",error="invalid_token",error_description="token verification failure"`
447
+ )
448
+ expect(handlerExecuted).toBeFalsy()
449
+ })
450
+
451
+ it('Should not authorize requests with malformed token structure in header', async () => {
452
+ const invalid_token = 'invalid token'
453
+ const url = 'http://localhost/auth-with-keys/a'
454
+ const req = new Request(url)
455
+ req.headers.set('Authorization', `Bearer ${invalid_token}`)
456
+ const res = await app.request(req)
457
+ expect(res).not.toBeNull()
458
+ expect(res.status).toBe(401)
459
+ expect(res.headers.get('www-authenticate')).toEqual(
460
+ `Bearer realm="${url}",error="invalid_request",error_description="invalid credentials structure"`
461
+ )
462
+ expect(handlerExecuted).toBeFalsy()
463
+ })
464
+
465
+ it('Should not authorize requests without authorization in nested JWK middleware', async () => {
466
+ const req = new Request('http://localhost/auth-with-keys-nested/a')
467
+ const res = await app.request(req)
468
+ expect(res).not.toBeNull()
469
+ expect(res.status).toBe(401)
470
+ expect(await res.text()).toBe('Unauthorized')
471
+ expect(handlerExecuted).toBeFalsy()
472
+ })
473
+
474
+ it('Should authorize requests with authorization in nested JWK middleware', async () => {
475
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
476
+ const req = new Request('http://localhost/auth-with-keys-nested/a')
477
+ req.headers.set('Authorization', `Bearer ${credential}`)
478
+ const res = await app.request(req)
479
+ expect(res).not.toBeNull()
480
+ expect(res.status).toBe(200)
481
+ expect(await res.json()).toEqual({ message: 'hello world' })
482
+ expect(handlerExecuted).toBeTruthy()
483
+ })
484
+ })
485
+
486
+ describe('Credentials in custom header', () => {
487
+ let handlerExecuted: boolean
488
+
489
+ beforeEach(() => {
490
+ handlerExecuted = false
491
+ })
492
+
493
+ const app = new App()
494
+
495
+ app.use(
496
+ '/auth-with-keys/*',
497
+ jwk({ keys: verify_keys, headerName: 'x-custom-auth-header', alg: ['RS256'] })
498
+ )
499
+
500
+ app.get('/auth-with-keys/*', (c) => {
501
+ handlerExecuted = true
502
+ const payload = c.get('jwtPayload')
503
+ return c.json(payload)
504
+ })
505
+
506
+ it('Should not authorize', async () => {
507
+ const req = new Request('http://localhost/auth-with-keys/a')
508
+ const res = await app.request(req)
509
+ expect(res).not.toBeNull()
510
+ expect(res.status).toBe(401)
511
+ expect(await res.text()).toBe('Unauthorized')
512
+ expect(handlerExecuted).toBeFalsy()
513
+ })
514
+
515
+ it('Should not authorize even if default authorization header present', async () => {
516
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
517
+
518
+ const req = new Request('http://localhost/auth-with-keys/a')
519
+ req.headers.set('Authorization', `Bearer ${credential}`)
520
+ const res = await app.request(req)
521
+ expect(res).not.toBeNull()
522
+ expect(res.status).toBe(401)
523
+ expect(await res.text()).toBe('Unauthorized')
524
+ expect(handlerExecuted).toBeFalsy()
525
+ })
526
+
527
+ it('Should authorize', async () => {
528
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[1])
529
+
530
+ const req = new Request('http://localhost/auth-with-keys/a')
531
+ req.headers.set('x-custom-auth-header', `Bearer ${credential}`)
532
+ const res = await app.request(req)
533
+ expect(res).not.toBeNull()
534
+ expect(res.status).toBe(200)
535
+ expect(await res.json()).toEqual({ message: 'hello world' })
536
+ expect(handlerExecuted).toBeTruthy()
537
+ })
538
+
539
+ it('Should authorize with mixed-case Bearer scheme in custom header', async () => {
540
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[1])
541
+
542
+ const req = new Request('http://localhost/auth-with-keys/a')
543
+ req.headers.set('x-custom-auth-header', `bEaReR ${credential}`)
544
+ const res = await app.request(req)
545
+ expect(res).not.toBeNull()
546
+ expect(res.status).toBe(200)
547
+ expect(await res.json()).toEqual({ message: 'hello world' })
548
+ expect(handlerExecuted).toBeTruthy()
549
+ })
550
+
551
+ it('Should not authorize with non-Bearer scheme in custom header', async () => {
552
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[1])
553
+ const url = 'http://localhost/auth-with-keys/a'
554
+
555
+ const req = new Request(url)
556
+ req.headers.set('x-custom-auth-header', `Basic ${credential}`)
557
+ const res = await app.request(req)
558
+ expect(res).not.toBeNull()
559
+ expect(res.status).toBe(401)
560
+ expect(res.headers.get('www-authenticate')).toEqual(
561
+ `Bearer realm="${url}",error="invalid_request",error_description="invalid credentials structure"`
562
+ )
563
+ expect(handlerExecuted).toBeFalsy()
564
+ })
565
+
566
+ it('Should not authorize invalid token with Bearer scheme in custom header', async () => {
567
+ const invalidToken =
568
+ 'ssyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
569
+ const url = 'http://localhost/auth-with-keys/a'
570
+
571
+ const req = new Request(url)
572
+ req.headers.set('x-custom-auth-header', `Bearer ${invalidToken}`)
573
+ const res = await app.request(req)
574
+ expect(res).not.toBeNull()
575
+ expect(res.status).toBe(401)
576
+ expect(res.headers.get('www-authenticate')).toEqual(
577
+ `Bearer realm="${url}",error="invalid_token",error_description="token verification failure"`
578
+ )
579
+ expect(handlerExecuted).toBeFalsy()
580
+ })
581
+ })
582
+
583
+ describe('Credentials in cookie', () => {
584
+ let handlerExecuted: boolean
585
+
586
+ beforeEach(() => {
587
+ handlerExecuted = false
588
+ })
589
+
590
+ const app = new App()
591
+
592
+ app.use('/auth-with-keys/*', jwk({ keys: verify_keys, cookie: 'access_token', alg: ['RS256'] }))
593
+ app.use(
594
+ '/auth-with-keys-unicode/*',
595
+ jwk({ keys: verify_keys, cookie: 'access_token', alg: ['RS256'] })
596
+ )
597
+ app.use(
598
+ '/auth-with-keys-prefixed/*',
599
+ jwk({
600
+ keys: verify_keys,
601
+ cookie: { key: 'access_token', prefixOptions: 'host' },
602
+ alg: ['RS256'],
603
+ })
604
+ )
605
+ app.use(
606
+ '/auth-with-keys-unprefixed/*',
607
+ jwk({ keys: verify_keys, cookie: { key: 'access_token' }, alg: ['RS256'] })
608
+ )
609
+
610
+ app.get('/auth-with-keys/*', (c) => {
611
+ handlerExecuted = true
612
+ const payload = c.get('jwtPayload')
613
+ return c.json(payload)
614
+ })
615
+ app.get('/auth-with-keys-prefixed/*', (c) => {
616
+ handlerExecuted = true
617
+ const payload = c.get('jwtPayload')
618
+ return c.json(payload)
619
+ })
620
+ app.get('/auth-with-keys-unprefixed/*', (c) => {
621
+ handlerExecuted = true
622
+ const payload = c.get('jwtPayload')
623
+ return c.json(payload)
624
+ })
625
+ app.get('/auth-with-keys-unicode/*', (c) => {
626
+ handlerExecuted = true
627
+ const payload = c.get('jwtPayload')
628
+ return c.json(payload)
629
+ })
630
+
631
+ it('Should not authorize requests with missing access token', async () => {
632
+ const req = new Request('http://localhost/auth-with-keys/a')
633
+ const res = await app.request(req)
634
+ expect(res).not.toBeNull()
635
+ expect(res.status).toBe(401)
636
+ expect(await res.text()).toBe('Unauthorized')
637
+ expect(handlerExecuted).toBeFalsy()
638
+ })
639
+
640
+ it('Should authorize cookie from a static array passed to options.keys', async () => {
641
+ const url = 'http://localhost/auth-with-keys/a'
642
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
643
+ const req = new Request(url, {
644
+ headers: new Headers({
645
+ Cookie: `access_token=${credential}`,
646
+ }),
647
+ })
648
+ const res = await app.request(req)
649
+ expect(res).not.toBeNull()
650
+ expect(await res.json()).toEqual({ message: 'hello world' })
651
+ expect(res.status).toBe(200)
652
+ expect(handlerExecuted).toBeTruthy()
653
+ })
654
+
655
+ it('Should authorize prefixed cookie from a static array passed to options.keys', async () => {
656
+ const url = 'http://localhost/auth-with-keys-prefixed/a'
657
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
658
+ const req = new Request(url, {
659
+ headers: new Headers({
660
+ Cookie: `__Host-access_token=${credential}`,
661
+ }),
662
+ })
663
+ const res = await app.request(req)
664
+ expect(res).not.toBeNull()
665
+ expect(await res.json()).toEqual({ message: 'hello world' })
666
+ expect(res.status).toBe(200)
667
+ expect(handlerExecuted).toBeTruthy()
668
+ })
669
+
670
+ it('Should authorize unprefixed cookie from a static array passed to options.keys', async () => {
671
+ const url = 'http://localhost/auth-with-keys-unprefixed/a'
672
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
673
+ const req = new Request(url, {
674
+ headers: new Headers({
675
+ Cookie: `access_token=${credential}`,
676
+ }),
677
+ })
678
+ const res = await app.request(req)
679
+ expect(res).not.toBeNull()
680
+ expect(await res.json()).toEqual({ message: 'hello world' })
681
+ expect(res.status).toBe(200)
682
+ expect(handlerExecuted).toBeTruthy()
683
+ })
684
+
685
+ it('Should authorize with Unicode payload from a static array passed to options.keys', async () => {
686
+ const credential = await Jwt.sign({ message: 'hello world' }, test_keys.private_keys[0])
687
+ const req = new Request('http://localhost/auth-with-keys-unicode/a', {
688
+ headers: new Headers({
689
+ Cookie: `access_token=${credential}`,
690
+ }),
691
+ })
692
+ const res = await app.request(req)
693
+ expect(res).not.toBeNull()
694
+ expect(res.status).toBe(200)
695
+ expect(await res.json()).toEqual({ message: 'hello world' })
696
+ expect(handlerExecuted).toBeTruthy()
697
+ })
698
+
699
+ it('Should not authorize requests with invalid Unicode payload in cookie', async () => {
700
+ const invalidToken =
701
+ 'ssyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE'
702
+
703
+ const url = 'http://localhost/auth-with-keys-unicode/a'
704
+ const req = new Request(url)
705
+ req.headers.set('Cookie', `access_token=${invalidToken}`)
706
+ const res = await app.request(req)
707
+ expect(res).not.toBeNull()
708
+ expect(res.status).toBe(401)
709
+ expect(res.headers.get('www-authenticate')).toEqual(
710
+ `Bearer realm="${url}",error="invalid_token",error_description="token verification failure"`
711
+ )
712
+ expect(handlerExecuted).toBeFalsy()
713
+ })
714
+
715
+ it('Should not authorize requests with malformed token structure in cookie', async () => {
716
+ const invalidToken = 'invalid token'
717
+ const url = 'http://localhost/auth-with-keys/a'
718
+ const req = new Request(url)
719
+ req.headers.set('Cookie', `access_token=${invalidToken}`)
720
+ const res = await app.request(req)
721
+ expect(res).not.toBeNull()
722
+ expect(res.status).toBe(401)
723
+ expect(res.headers.get('www-authenticate')).toEqual(
724
+ `Bearer realm="${url}",error="invalid_token",error_description="token verification failure"`
725
+ )
726
+ expect(handlerExecuted).toBeFalsy()
727
+ })
728
+ })
729
+
730
+ describe('Credentials in a signed cookie', () => {
731
+ let handlerExecuted: boolean
732
+
733
+ beforeEach(() => {
734
+ handlerExecuted = false
735
+ })
736
+
737
+ const app = new App()
738
+ const test_secret = 'Shhh'
739
+
740
+ app.use(
741
+ '/auth-with-signed-cookie/*',
742
+ jwk({
743
+ keys: verify_keys,
744
+ cookie: { key: 'access_token', secret: test_secret },
745
+ alg: ['RS256'],
746
+ })
747
+ )
748
+ app.use(
749
+ '/auth-with-signed-with-prefix-options-cookie/*',
750
+ jwk({
751
+ keys: verify_keys,
752
+ cookie: { key: 'access_token', secret: test_secret, prefixOptions: 'host' },
753
+ alg: ['RS256'],
754
+ })
755
+ )
756
+
757
+ app.get('/sign-cookie', async (c) => {
758
+ const credential = await Jwt.sign(
759
+ { message: 'signed hello world' },
760
+ test_keys.private_keys[0]
761
+ )
762
+ await setSignedCookie(c, 'access_token', credential, test_secret)
763
+ return c.text('OK')
764
+ })
765
+ app.get('/sign-cookie-with-prefix', async (c) => {
766
+ const credential = await Jwt.sign(
767
+ { message: 'signed hello world' },
768
+ test_keys.private_keys[0]
769
+ )
770
+ await setSignedCookie(c, 'access_token', credential, test_secret, { prefix: 'host' })
771
+ return c.text('OK')
772
+ })
773
+ app.get('/auth-with-signed-cookie/*', async (c) => {
774
+ handlerExecuted = true
775
+ const payload = c.get('jwtPayload')
776
+ return c.json(payload)
777
+ })
778
+ app.get('/auth-with-signed-with-prefix-options-cookie/*', async (c) => {
779
+ handlerExecuted = true
780
+ const payload = c.get('jwtPayload')
781
+ return c.json(payload)
782
+ })
783
+
784
+ it('Should authorize signed cookie', async () => {
785
+ const url = 'http://localhost/auth-with-signed-cookie/a'
786
+ const sign_res = await app.request('http://localhost/sign-cookie')
787
+ const cookieHeader = sign_res.headers.get('Set-Cookie') as string
788
+ expect(cookieHeader).not.toBeNull()
789
+ const req = new Request(url)
790
+ req.headers.set('Cookie', cookieHeader)
791
+ const res = await app.request(req)
792
+ expect(res).not.toBeNull()
793
+ expect(res.status).toBe(200)
794
+ expect(await res.json()).toEqual({ message: 'signed hello world' })
795
+ expect(handlerExecuted).toBeTruthy()
796
+ })
797
+
798
+ it('Should authorize prefixed signed cookie', async () => {
799
+ const url = 'http://localhost/auth-with-signed-with-prefix-options-cookie/a'
800
+ const sign_res = await app.request('http://localhost/sign-cookie-with-prefix')
801
+ const cookieHeader = sign_res.headers.get('Set-Cookie') as string
802
+ expect(cookieHeader).not.toBeNull()
803
+ const req = new Request(url)
804
+ req.headers.set('Cookie', cookieHeader)
805
+ const res = await app.request(req)
806
+ expect(res).not.toBeNull()
807
+ expect(res.status).toBe(200)
808
+ expect(await res.json()).toEqual({ message: 'signed hello world' })
809
+ expect(handlerExecuted).toBeTruthy()
810
+ })
811
+
812
+ it('Should not authorize an unsigned cookie', async () => {
813
+ const url = 'http://localhost/auth-with-signed-cookie/a'
814
+ const credential = await Jwt.sign(
815
+ { message: 'unsigned hello world' },
816
+ test_keys.private_keys[0]
817
+ )
818
+ const unsignedCookie = `access_token=${credential}`
819
+ const req = new Request(url)
820
+ req.headers.set('Cookie', unsignedCookie)
821
+ const res = await app.request(req)
822
+ expect(res.status).toBe(401)
823
+ expect(await res.text()).toBe('Unauthorized')
824
+ expect(handlerExecuted).toBeFalsy()
825
+ })
826
+ })
827
+
828
+ describe('Error handling with `cause`', () => {
829
+ const app = new App()
830
+
831
+ app.use('/auth-with-keys/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
832
+ app.get('/auth-with-keys/*', (c) => c.text('Authorized'))
833
+
834
+ app.onError((e, c) => {
835
+ if (e instanceof HTTPException && e.cause instanceof Error) {
836
+ return c.json({ name: e.cause.name, message: e.cause.message }, 401)
837
+ }
838
+ return c.text(e.message, 401)
839
+ })
840
+
841
+ it('Should not authorize', async () => {
842
+ const credential = 'abc.def.ghi'
843
+ const req = new Request('http://localhost/auth-with-keys')
844
+ req.headers.set('Authorization', `Bearer ${credential}`)
845
+ const res = await app.request(req)
846
+ expect(res.status).toBe(401)
847
+ expect(await res.json()).toEqual({
848
+ name: 'JwtTokenInvalid',
849
+ message: `invalid JWT token: ${credential}`,
850
+ })
851
+ })
852
+ })
853
+
854
+ describe('Verification of token attributes', () => {
855
+ let handlerExecuted: boolean
856
+
857
+ beforeEach(() => {
858
+ handlerExecuted = false
859
+ })
860
+
861
+ function inFuture() {
862
+ return Date.now() / 1000 + 100
863
+ }
864
+
865
+ function inPast() {
866
+ return Date.now() / 1000 - 100
867
+ }
868
+
869
+ const app = new App()
870
+
871
+ app.use('/auth-with-keys-default/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
872
+ app.use(
873
+ '/auth-with-keys-and-issuer/*',
874
+ jwk({ keys: verify_keys, verification: { iss: 'http://issuer.test' }, alg: ['RS256'] })
875
+ )
876
+
877
+ app.get('/auth-with-keys-default/*', (c) => {
878
+ handlerExecuted = true
879
+ const payload = c.get('jwtPayload')
880
+ return c.json(payload)
881
+ })
882
+ app.get('/auth-with-keys-and-issuer/*', (c) => {
883
+ handlerExecuted = true
884
+ const payload = c.get('jwtPayload')
885
+ return c.json(payload)
886
+ })
887
+
888
+ it('Should validate exp/nbf/iat and pass when good by default', async () => {
889
+ const payload = {
890
+ exp: inFuture(),
891
+ nbf: inPast(),
892
+ iat: inPast(),
893
+ iss: 'http://not-checked.test',
894
+ }
895
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
896
+ const req = new Request('http://localhost/auth-with-keys-default/a')
897
+ req.headers.set('Authorization', `Bearer ${credential}`)
898
+ const res = await app.request(req)
899
+ expect(res).not.toBeNull()
900
+ expect(res.status).toBe(200)
901
+ expect(await res.json()).toEqual(payload)
902
+ expect(handlerExecuted).toBeTruthy()
903
+ })
904
+
905
+ it('Should validate exp and fail when bad', async () => {
906
+ const payload = {
907
+ exp: inPast(),
908
+ }
909
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
910
+ const req = new Request('http://localhost/auth-with-keys-default/a')
911
+ req.headers.set('Authorization', `Bearer ${credential}`)
912
+ const res = await app.request(req)
913
+ expect(res).not.toBeNull()
914
+ expect(res.status).toBe(401)
915
+ expect(handlerExecuted).toBeFalsy()
916
+ })
917
+
918
+ it('Should validate nbf and fail when bad', async () => {
919
+ const payload = {
920
+ nbf: inFuture(),
921
+ }
922
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
923
+ const req = new Request('http://localhost/auth-with-keys-default/a')
924
+ req.headers.set('Authorization', `Bearer ${credential}`)
925
+ const res = await app.request(req)
926
+ expect(res).not.toBeNull()
927
+ expect(res.status).toBe(401)
928
+ expect(handlerExecuted).toBeFalsy()
929
+ })
930
+
931
+ it('Should validate iat and fail when bad', async () => {
932
+ const payload = {
933
+ iat: inFuture(),
934
+ }
935
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
936
+ const req = new Request('http://localhost/auth-with-keys-default/a')
937
+ req.headers.set('Authorization', `Bearer ${credential}`)
938
+ const res = await app.request(req)
939
+ expect(res).not.toBeNull()
940
+ expect(res.status).toBe(401)
941
+ expect(handlerExecuted).toBeFalsy()
942
+ })
943
+
944
+ it('Should validate iss when supplied', async () => {
945
+ const payload = {
946
+ iss: 'http://issuer.test',
947
+ }
948
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
949
+ const req = new Request('http://localhost/auth-with-keys-and-issuer/a')
950
+ req.headers.set('Authorization', `Bearer ${credential}`)
951
+ const res = await app.request(req)
952
+ expect(res).not.toBeNull()
953
+ expect(res.status).toBe(200)
954
+ expect(await res.json()).toEqual(payload)
955
+ expect(handlerExecuted).toBeTruthy()
956
+ })
957
+
958
+ it('Should reject missing iss when required', async () => {
959
+ const payload = {
960
+ // Nothing
961
+ }
962
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
963
+ const req = new Request('http://localhost/auth-with-keys-and-issuer/a')
964
+ req.headers.set('Authorization', `Bearer ${credential}`)
965
+ const res = await app.request(req)
966
+ expect(res).not.toBeNull()
967
+ expect(res.status).toBe(401)
968
+ expect(handlerExecuted).toBeFalsy()
969
+ })
970
+
971
+ it('Should reject iss when different', async () => {
972
+ const payload = {
973
+ iss: 'http://bad-issuer.test',
974
+ }
975
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0])
976
+ const req = new Request('http://localhost/auth-with-keys-and-issuer/a')
977
+ req.headers.set('Authorization', `Bearer ${credential}`)
978
+ const res = await app.request(req)
979
+ expect(res).not.toBeNull()
980
+ expect(res.status).toBe(401)
981
+ expect(handlerExecuted).toBeFalsy()
982
+ })
983
+ })
984
+
985
+ describe('Algorithm whitelist (options.alg)', () => {
986
+ let handlerExecuted: boolean
987
+
988
+ beforeEach(() => {
989
+ handlerExecuted = false
990
+ })
991
+
992
+ const app = new App()
993
+
994
+ // Only allow RS256
995
+ app.use('/auth-whitelist-rs256/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
996
+ app.get('/auth-whitelist-rs256/*', (c) => {
997
+ handlerExecuted = true
998
+ return c.json(c.get('jwtPayload'))
999
+ })
1000
+
1001
+ // Allow multiple algorithms
1002
+ app.use('/auth-whitelist-multi/*', jwk({ keys: verify_keys, alg: ['RS256', 'ES256'] }))
1003
+ app.get('/auth-whitelist-multi/*', (c) => {
1004
+ handlerExecuted = true
1005
+ return c.json(c.get('jwtPayload'))
1006
+ })
1007
+
1008
+ // Note: Test for "no whitelist" was removed because alg is now required.
1009
+ // This is a breaking change that enforces explicit algorithm specification for security.
1010
+
1011
+ it('Should authorize RS256 token when RS256 is in whitelist', async () => {
1012
+ const payload = { message: 'hello world' }
1013
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0]) // RS256 key
1014
+ const req = new Request('http://localhost/auth-whitelist-rs256/a')
1015
+ req.headers.set('Authorization', `Bearer ${credential}`)
1016
+ const res = await app.request(req)
1017
+ expect(res).not.toBeNull()
1018
+ expect(res.status).toBe(200)
1019
+ expect(await res.json()).toEqual(payload)
1020
+ expect(handlerExecuted).toBeTruthy()
1021
+ })
1022
+
1023
+ it('Should reject token when algorithm is not in whitelist', async () => {
1024
+ // Create a token with ES256 algorithm manually
1025
+ const kid = 'acryl-test-kid-1' // Use existing kid but header will have different alg
1026
+ const payload = { message: 'hello world' }
1027
+
1028
+ // Generate ES256 key pair for signing
1029
+ const keyPair = await crypto.subtle.generateKey(
1030
+ {
1031
+ name: 'ECDSA',
1032
+ namedCurve: 'P-256',
1033
+ },
1034
+ true,
1035
+ ['sign', 'verify']
1036
+ )
1037
+
1038
+ // Create JWT with ES256
1039
+ const header = { alg: 'ES256', typ: 'JWT', kid }
1040
+ const encode = (obj: object) =>
1041
+ encodeBase64Url(utf8Encoder.encode(JSON.stringify(obj)).buffer)
1042
+ const encodedHeader = encode(header)
1043
+ const encodedPayload = encode(payload)
1044
+ const signingInput = `${encodedHeader}.${encodedPayload}`
1045
+
1046
+ const signatureBuffer = await signing(
1047
+ keyPair.privateKey,
1048
+ 'ES256',
1049
+ utf8Encoder.encode(signingInput)
1050
+ )
1051
+ const signature = encodeBase64Url(signatureBuffer)
1052
+
1053
+ const token = `${encodedHeader}.${encodedPayload}.${signature}`
1054
+
1055
+ const url = 'http://localhost/auth-whitelist-rs256/a'
1056
+ const req = new Request(url)
1057
+ req.headers.set('Authorization', `Bearer ${token}`)
1058
+ const res = await app.request(req)
1059
+ expect(res).not.toBeNull()
1060
+ expect(res.status).toBe(401)
1061
+ expect(res.headers.get('www-authenticate')).toMatch(/token verification failure/)
1062
+ expect(handlerExecuted).toBeFalsy()
1063
+ })
1064
+
1065
+ it('Should authorize RS256 token when multiple algorithms are in whitelist', async () => {
1066
+ const payload = { message: 'hello world' }
1067
+ const credential = await Jwt.sign(payload, test_keys.private_keys[0]) // RS256 key
1068
+ const req = new Request('http://localhost/auth-whitelist-multi/a')
1069
+ req.headers.set('Authorization', `Bearer ${credential}`)
1070
+ const res = await app.request(req)
1071
+ expect(res).not.toBeNull()
1072
+ expect(res.status).toBe(200)
1073
+ expect(await res.json()).toEqual(payload)
1074
+ expect(handlerExecuted).toBeTruthy()
1075
+ })
1076
+
1077
+ // Note: Test for "no whitelist" was removed because alg is now required.
1078
+ // This is a breaking change that enforces explicit algorithm specification for security.
1079
+ })
1080
+ describe('WWW-Authenticate realm', () => {
1081
+ it('Should default to the request URL', async () => {
1082
+ const app = new App()
1083
+ app.use('/auth/*', jwk({ keys: verify_keys, alg: ['RS256'] }))
1084
+ app.get('/auth/*', (c) => c.text('ok'))
1085
+
1086
+ const res = await app.request('http://localhost/auth/page?a=1')
1087
+
1088
+ expect(res.status).toBe(401)
1089
+ expect(res.headers.get('WWW-Authenticate')).toBe(
1090
+ 'Bearer realm="http://localhost/auth/page?a=1",error="invalid_request",error_description="no authorization included in request"'
1091
+ )
1092
+ })
1093
+
1094
+ it('Should use the configured realm', async () => {
1095
+ const app = new App()
1096
+ app.use('/auth/*', jwk({ keys: verify_keys, alg: ['RS256'], realm: 'my-api' }))
1097
+ app.get('/auth/*', (c) => c.text('ok'))
1098
+
1099
+ const res = await app.request('http://localhost/auth/page?a=1')
1100
+
1101
+ expect(res.status).toBe(401)
1102
+ expect(res.headers.get('WWW-Authenticate')).toBe(
1103
+ 'Bearer realm="my-api",error="invalid_request",error_description="no authorization included in request"'
1104
+ )
1105
+ })
1106
+
1107
+ it('Should use the configured realm for an invalid token', async () => {
1108
+ const app = new App()
1109
+ app.use('/auth/*', jwk({ keys: verify_keys, alg: ['RS256'], realm: 'my-api' }))
1110
+ app.get('/auth/*', (c) => c.text('ok'))
1111
+
1112
+ const req = new Request('http://localhost/auth/page')
1113
+ req.headers.set('Authorization', 'Bearer invalid-token')
1114
+ const res = await app.request(req)
1115
+
1116
+ expect(res.status).toBe(401)
1117
+ expect(res.headers.get('WWW-Authenticate')).toBe(
1118
+ 'Bearer realm="my-api",error="invalid_token",error_description="token verification failure"'
1119
+ )
1120
+ })
1121
+
1122
+ it('Should escape double quotes in the realm', async () => {
1123
+ const app = new App()
1124
+ app.use('/auth/*', jwk({ keys: verify_keys, alg: ['RS256'], realm: 'my "quoted" api' }))
1125
+ app.get('/auth/*', (c) => c.text('ok'))
1126
+
1127
+ const res = await app.request('http://localhost/auth/page')
1128
+
1129
+ expect(res.status).toBe(401)
1130
+ expect(res.headers.get('WWW-Authenticate')).toBe(
1131
+ 'Bearer realm="my \\"quoted\\" api",error="invalid_request",error_description="no authorization included in request"'
1132
+ )
1133
+ })
1134
+ })
1135
+ })