acrylblends 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/dist/index.d.ts +6 -1
  2. package/dist/index.js +84 -5
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/LICENSE +23 -0
  5. package/docs/framework/README.md +63 -0
  6. package/docs/framework/UPSTREAM.md +3 -0
  7. package/docs/framework/api-summary.md +138 -0
  8. package/docs/framework/concepts.md +105 -0
  9. package/docs/framework/context.md +129 -0
  10. package/docs/framework/getting-started.md +146 -0
  11. package/docs/framework/helpers.md +118 -0
  12. package/docs/framework/middleware.md +154 -0
  13. package/docs/framework/migration-from-0-3.md +108 -0
  14. package/docs/framework/migration-from-dsh-webserver.md +103 -0
  15. package/docs/framework/node-server.md +75 -0
  16. package/docs/framework/plugin-lifecycle.md +111 -0
  17. package/docs/framework/request.md +106 -0
  18. package/docs/framework/routing.md +188 -0
  19. package/docs/framework/surfaces.md +71 -0
  20. package/docs/framework/upstream.json +9 -0
  21. package/docs/framework/validation.md +135 -0
  22. package/package.json +4 -2
  23. package/plugins/acryl-inspect/plugin.json +2 -1
  24. package/plugins/acryl-inspect/src/report.ts +21 -13
  25. package/plugins/acryl-inspect/src/service.ts +9 -7
  26. package/plugins/acryl-inspect/tests/report.spec.ts +13 -8
  27. package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
  28. package/plugins/acryl-inspect/upstream.json +2 -2
  29. package/plugins/acryl-node-server/LICENSE +26 -0
  30. package/plugins/acryl-node-server/plugin.json +2 -1
  31. package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
  32. package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
  33. package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
  34. package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
  35. package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
  36. package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
  37. package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
  38. package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
  39. package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
  40. package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
  41. package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
  42. package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
  43. package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
  44. package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
  45. package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
  46. package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
  47. package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
  48. package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
  49. package/plugins/acryl-node-server/src/index.ts +19 -6
  50. package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
  51. package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
  52. package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
  53. package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
  54. package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
  55. package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
  56. package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
  57. package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
  58. package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
  59. package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
  60. package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
  61. package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
  62. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
  63. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
  64. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
  65. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
  66. package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
  67. package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
  68. package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
  69. package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
  70. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
  71. package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
  72. package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
  73. package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
  74. package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
  75. package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
  76. package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
  77. package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
  78. package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
  79. package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
  80. package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
  81. package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
  82. package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
  83. package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
  84. package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
  85. package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
  86. package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
  87. package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
  88. package/plugins/acryl-node-server/upstream.json +3 -3
  89. package/plugins/acryl-router/LICENSE +26 -0
  90. package/plugins/acryl-router/plugin.json +2 -1
  91. package/plugins/acryl-router/src/client.ts +2 -0
  92. package/plugins/acryl-router/src/connection.ts +42 -0
  93. package/plugins/acryl-router/src/core/app-base.ts +544 -0
  94. package/plugins/acryl-router/src/core/app.test.ts +3833 -0
  95. package/plugins/acryl-router/src/core/app.ts +34 -0
  96. package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
  97. package/plugins/acryl-router/src/core/client/client.ts +249 -0
  98. package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
  99. package/plugins/acryl-router/src/core/client/index.ts +17 -0
  100. package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
  101. package/plugins/acryl-router/src/core/client/types.ts +393 -0
  102. package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
  103. package/plugins/acryl-router/src/core/client/utils.ts +117 -0
  104. package/plugins/acryl-router/src/core/compose.test.ts +751 -0
  105. package/plugins/acryl-router/src/core/compose.ts +73 -0
  106. package/plugins/acryl-router/src/core/context.test.ts +629 -0
  107. package/plugins/acryl-router/src/core/context.ts +781 -0
  108. package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
  109. package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
  110. package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
  111. package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
  112. package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
  113. package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
  114. package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
  115. package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
  116. package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
  117. package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
  118. package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
  119. package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
  120. package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
  121. package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
  122. package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
  123. package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
  124. package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
  125. package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
  126. package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
  127. package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
  128. package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
  129. package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
  130. package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
  131. package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
  132. package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
  133. package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
  134. package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
  135. package/plugins/acryl-router/src/core/http-exception.ts +77 -0
  136. package/plugins/acryl-router/src/core/index.ts +53 -0
  137. package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
  138. package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
  139. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
  140. package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
  141. package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
  142. package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
  143. package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
  144. package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
  145. package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
  146. package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
  147. package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
  148. package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
  149. package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
  150. package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
  151. package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
  152. package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
  153. package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
  154. package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
  155. package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
  156. package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
  157. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
  158. package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
  159. package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
  160. package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
  161. package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
  162. package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
  163. package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
  164. package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
  165. package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
  166. package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
  167. package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
  168. package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
  169. package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
  170. package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
  171. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
  172. package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
  173. package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
  174. package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
  175. package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
  176. package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
  177. package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
  178. package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
  179. package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
  180. package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
  181. package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
  182. package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
  183. package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
  184. package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
  185. package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
  186. package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
  187. package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
  188. package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
  189. package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
  190. package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
  191. package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
  192. package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
  193. package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
  194. package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
  195. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
  196. package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
  197. package/plugins/acryl-router/src/core/request/constants.ts +1 -0
  198. package/plugins/acryl-router/src/core/request.test.ts +751 -0
  199. package/plugins/acryl-router/src/core/request.ts +502 -0
  200. package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
  201. package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
  202. package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
  203. package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
  204. package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
  205. package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
  206. package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
  207. package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
  208. package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
  209. package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
  210. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
  211. package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
  212. package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
  213. package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
  214. package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
  215. package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
  216. package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
  217. package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
  218. package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
  219. package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
  220. package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
  221. package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
  222. package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
  223. package/plugins/acryl-router/src/core/router/utils.ts +1 -0
  224. package/plugins/acryl-router/src/core/router.ts +103 -0
  225. package/plugins/acryl-router/src/core/types.test.ts +4134 -0
  226. package/plugins/acryl-router/src/core/types.ts +2782 -0
  227. package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
  228. package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
  229. package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
  230. package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
  231. package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
  232. package/plugins/acryl-router/src/core/utils/body.ts +263 -0
  233. package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
  234. package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
  235. package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
  236. package/plugins/acryl-router/src/core/utils/color.ts +56 -0
  237. package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
  238. package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
  239. package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
  240. package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
  241. package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
  242. package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
  243. package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
  244. package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
  245. package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
  246. package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
  247. package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
  248. package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
  249. package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
  250. package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
  251. package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
  252. package/plugins/acryl-router/src/core/utils/html.ts +182 -0
  253. package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
  254. package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
  255. package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
  256. package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
  257. package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
  258. package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
  259. package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
  260. package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
  261. package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
  262. package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
  263. package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
  264. package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
  265. package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
  266. package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
  267. package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
  268. package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
  269. package/plugins/acryl-router/src/core/utils/types.ts +123 -0
  270. package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
  271. package/plugins/acryl-router/src/core/utils/url.ts +337 -0
  272. package/plugins/acryl-router/src/core/validator/index.ts +8 -0
  273. package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
  274. package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
  275. package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
  276. package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
  277. package/plugins/acryl-router/src/helpers/index.ts +13 -3
  278. package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
  279. package/plugins/acryl-router/src/index.ts +6 -6
  280. package/plugins/acryl-router/src/middleware/index.ts +25 -6
  281. package/plugins/acryl-router/src/middleware/loopback-only.ts +53 -14
  282. package/plugins/acryl-router/src/service.ts +262 -64
  283. package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
  284. package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
  285. package/plugins/acryl-router/tests/loader.spec.ts +3 -2
  286. package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
  287. package/plugins/acryl-router/tests/service.spec.ts +281 -66
  288. package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
  289. package/plugins/acryl-router/upstream.json +2 -2
  290. package/plugins/acryl-router-http/plugin.json +2 -1
  291. package/plugins/acryl-router-http/src/bridge.ts +3 -1
  292. package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
  293. package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
  294. package/plugins/acryl-router-http/upstream.json +3 -3
  295. package/plugins/acryl-state/NOTICE +30 -0
  296. package/plugins/acryl-state/plugin.json +1 -1
  297. package/plugins/acryl-state/src/cell.ts +1 -1
  298. package/plugins/acryl-state/src/service.ts +33 -15
  299. package/plugins/acryl-state/upstream.json +6 -4
  300. package/plugins/builder/script.ts +4 -7
  301. package/plugins/extensions/index.ts +6 -5
  302. package/plugins/extensions/resolve.ts +28 -3
  303. package/plugins/openai/prompt.ts +9 -7
  304. package/plugins/tsconfig.json +5 -4
  305. package/shared/AGENTS.core.md +54 -0
  306. package/templates/agent/AGENTS.md +6 -0
  307. package/templates/agent/README.md +2 -2
  308. package/templates/agent/blend.yaml +1 -1
  309. package/templates/agent/src/routes.ts +37 -27
  310. package/templates/agent/test/model.test.ts +2 -1
  311. package/templates/agent/tsconfig.json +8 -5
  312. package/templates/blank/AGENTS.md +5 -0
  313. package/templates/blank/README.md +1 -1
  314. package/templates/blank/src/routes.ts +6 -4
  315. package/templates/blank/tsconfig.json +2 -4
  316. package/templates/todo/AGENTS.md +5 -0
  317. package/templates/todo/src/page.ts +1 -1
  318. package/templates/todo/src/todo.ts +58 -29
  319. package/templates/todo/tsconfig.json +2 -4
  320. package/templates/todo/upstream.json +2 -2
  321. package/plugins/acryl-router/src/compose.ts +0 -21
  322. package/plugins/acryl-router/src/context.ts +0 -59
  323. package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
  324. package/plugins/acryl-router/src/helpers/sse.ts +0 -83
  325. package/plugins/acryl-router/src/helpers/validate.ts +0 -44
  326. package/plugins/acryl-router/src/http-error.ts +0 -21
  327. package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
  328. package/plugins/acryl-router/src/middleware/cors.ts +0 -46
  329. package/plugins/acryl-router/src/middleware/logger.ts +0 -21
  330. package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
  331. package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
  332. package/plugins/acryl-router/src/pattern.ts +0 -89
  333. package/plugins/acryl-router/src/registrar.ts +0 -90
  334. package/plugins/acryl-router/src/table.ts +0 -156
  335. package/plugins/acryl-router/src/types.ts +0 -73
  336. package/plugins/acryl-router/src/util.ts +0 -23
  337. package/plugins/acryl-router/tests/api.spec.ts +0 -223
  338. package/plugins/acryl-router/tests/conformance.ts +0 -170
  339. package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
  340. package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
  341. package/plugins/acryl-router/tests/table.spec.ts +0 -47
@@ -0,0 +1,503 @@
1
+ import { RETAINED_304_HEADERS, etag } from './index.ts'
2
+ import { App } from '../../app.ts'
3
+
4
+ const createPatternedBody = (length: number) => {
5
+ const body = new Uint8Array(length)
6
+ for (let i = 0; i < body.byteLength; i++) {
7
+ body[i] = i % 256
8
+ }
9
+ return body
10
+ }
11
+
12
+ describe('Etag Middleware', () => {
13
+ it('Should return etag header', async () => {
14
+ const app = new App()
15
+ app.use('/etag/*', etag())
16
+ app.get('/etag/abc', (c) => {
17
+ return c.text('App is hot')
18
+ })
19
+ app.get('/etag/def', (c) => {
20
+ return c.json({ message: 'App is hot' })
21
+ })
22
+ let res = await app.request('http://localhost/etag/abc')
23
+ expect(res.headers.get('ETag')).not.toBeFalsy()
24
+ expect(res.headers.get('ETag')).toBe('"7c6c452b40ba15d4748e0075f4e402b441c3ea15"')
25
+
26
+ res = await app.request('http://localhost/etag/def')
27
+ expect(res.headers.get('ETag')).not.toBeFalsy()
28
+ expect(res.headers.get('ETag')).toBe('"a6c237463eac08cd32a99f595cc2a7c87aecaadc"')
29
+ })
30
+
31
+ it('Should return etag header with another algorithm', async () => {
32
+ const app = new App()
33
+ app.use(
34
+ '/etag/*',
35
+ etag({
36
+ generateDigest: (body) =>
37
+ crypto.subtle.digest(
38
+ {
39
+ name: 'SHA-256',
40
+ },
41
+ body
42
+ ),
43
+ })
44
+ )
45
+ app.get('/etag/abc', (c) => {
46
+ return c.text('App is hot')
47
+ })
48
+ app.get('/etag/def', (c) => {
49
+ return c.json({ message: 'App is hot' })
50
+ })
51
+ let res = await app.request('http://localhost/etag/abc')
52
+ expect(res.headers.get('ETag')).not.toBeFalsy()
53
+ expect(res.headers.get('ETag')).toBe(
54
+ '"dc6a4486c5794ab001967920a901c34bde9f9485c77eccf142e13b446c6da381"'
55
+ )
56
+
57
+ res = await app.request('http://localhost/etag/def')
58
+ expect(res.headers.get('ETag')).not.toBeFalsy()
59
+ expect(res.headers.get('ETag')).toBe(
60
+ '"f467464463b028d082ec4327b42c87428502dd71740110d5e5b4e2fbcd8b366d"'
61
+ )
62
+ })
63
+
64
+ it('Should return etag header - binary', async () => {
65
+ const app = new App()
66
+ app.use('/etag/*', etag())
67
+ app.get('/etag', async (c) => {
68
+ return c.body(new Uint8Array(1))
69
+ })
70
+
71
+ const res = await app.request('http://localhost/etag')
72
+ expect(res.headers.get('ETag')).not.toBeFalsy()
73
+ const etagHeader = res.headers.get('ETag')
74
+ expect(etagHeader).toBe('"5ba93c9db0cff93f52b521d7420e43f6eda2784f"')
75
+ })
76
+
77
+ it('Should not be the same etag - arrayBuffer', async () => {
78
+ const app = new App()
79
+ app.use('/etag/*', etag())
80
+ app.get('/etag/ab1', (c) => {
81
+ return c.body(new ArrayBuffer(1))
82
+ })
83
+ app.get('/etag/ab2', (c) => {
84
+ return c.body(new ArrayBuffer(2))
85
+ })
86
+
87
+ let res = await app.request('http://localhost/etag/ab1')
88
+ const hash = res.headers.get('Etag')
89
+ res = await app.request('http://localhost/etag/ab2')
90
+ expect(res.headers.get('ETag')).not.toBe(hash)
91
+ })
92
+
93
+ it('Should not be the same etag - Uint8Array', async () => {
94
+ const app = new App()
95
+ app.use('/etag/*', etag())
96
+ app.get('/etag/ui1', (c) => {
97
+ return c.body(new Uint8Array([1, 2, 3]))
98
+ })
99
+ app.get('/etag/ui2', (c) => {
100
+ return c.body(new Uint8Array([1, 2, 3, 4]))
101
+ })
102
+
103
+ let res = await app.request('http://localhost/etag/ui1')
104
+ const hash = res.headers.get('Etag')
105
+ res = await app.request('http://localhost/etag/ui2')
106
+ expect(res.headers.get('ETag')).not.toBe(hash)
107
+ })
108
+
109
+ it('Should not be the same etag - ReadableStream', async () => {
110
+ const app = new App()
111
+ app.use('/etag/*', etag())
112
+ app.get('/etag/rs1', (c) => {
113
+ return c.body(
114
+ new ReadableStream({
115
+ start(controller) {
116
+ controller.enqueue(new Uint8Array([1]))
117
+ controller.enqueue(new Uint8Array([2]))
118
+ controller.close()
119
+ },
120
+ })
121
+ )
122
+ })
123
+ app.get('/etag/rs2', (c) => {
124
+ return c.body(
125
+ new ReadableStream({
126
+ start(controller) {
127
+ controller.enqueue(new Uint8Array([1]))
128
+ controller.enqueue(new Uint8Array([3]))
129
+ controller.close()
130
+ },
131
+ })
132
+ )
133
+ })
134
+
135
+ let res = await app.request('http://localhost/etag/rs1')
136
+ const hash = res.headers.get('Etag')
137
+ res = await app.request('http://localhost/etag/rs2')
138
+ expect(res.headers.get('ETag')).not.toBe(hash)
139
+ })
140
+
141
+ it('Should return the same etag regardless of ReadableStream chunk boundaries', async () => {
142
+ const app = new App()
143
+ app.use('/etag/*', etag())
144
+ const body = createPatternedBody(1_000_000)
145
+ app.get('/etag/rs1', (c) => {
146
+ return c.body(
147
+ new ReadableStream({
148
+ start(controller) {
149
+ controller.enqueue(body)
150
+ controller.close()
151
+ },
152
+ })
153
+ )
154
+ })
155
+ app.get('/etag/rs2', (c) => {
156
+ return c.body(
157
+ new ReadableStream({
158
+ start(controller) {
159
+ controller.enqueue(body.slice(0, 1))
160
+ controller.enqueue(body.slice(1, 32_769))
161
+ controller.enqueue(body.slice(32_769))
162
+ controller.close()
163
+ },
164
+ })
165
+ )
166
+ })
167
+
168
+ const res1 = await app.request('http://localhost/etag/rs1')
169
+ const res2 = await app.request('http://localhost/etag/rs2')
170
+ const expected = '"550563e0460b33a3540278a8446a70bb96eb5172"'
171
+ expect(res1.headers.get('ETag')).toBe(expected)
172
+ expect(res2.headers.get('ETag')).toBe(expected)
173
+ })
174
+
175
+ it('Should preserve the SHA-1 digest for bodies up to 256 KiB', async () => {
176
+ const app = new App()
177
+ app.use('/etag/*', etag())
178
+ app.get('/etag', (c) => c.body(createPatternedBody(256 * 1024)))
179
+
180
+ const res = await app.request('http://localhost/etag')
181
+ expect(res.headers.get('ETag')).toBe('"37ef77696fc255bf53b4cdd014b223676f2dc8bb"')
182
+ })
183
+
184
+ it('Should not return etag header when the stream is empty', async () => {
185
+ const app = new App()
186
+ app.use('/etag/*', etag())
187
+ app.get('/etag/abc', (c) => {
188
+ const stream = new ReadableStream({
189
+ start(controller) {
190
+ controller.close()
191
+ },
192
+ })
193
+ return c.body(stream)
194
+ })
195
+ const res = await app.request('http://localhost/etag/abc')
196
+ expect(res.status).toBe(200)
197
+ expect(res.headers.get('ETag')).toBeNull()
198
+ })
199
+
200
+ it('Should not return etag header when body is null', async () => {
201
+ const app = new App()
202
+ app.use('/etag/*', etag())
203
+ app.get('/etag/abc', () => new Response(null, { status: 500 }))
204
+ const res = await app.request('http://localhost/etag/abc')
205
+ expect(res.status).toBe(500)
206
+ expect(res.headers.get('ETag')).toBeNull()
207
+ })
208
+
209
+ it('Should return etag header - weak', async () => {
210
+ const app = new App()
211
+ app.use('/etag/*', etag({ weak: true }))
212
+ app.get('/etag/abc', (c) => {
213
+ return c.text('App is hot')
214
+ })
215
+
216
+ const res = await app.request('http://localhost/etag/abc')
217
+ expect(res.headers.get('ETag')).not.toBeFalsy()
218
+ expect(res.headers.get('ETag')).toBe('W/"7c6c452b40ba15d4748e0075f4e402b441c3ea15"')
219
+ })
220
+
221
+ it('Should handle conditional GETs', async () => {
222
+ const app = new App()
223
+ app.use('/etag/*', etag())
224
+ app.get('/etag/ghi', (c) =>
225
+ c.text('App is great', 200, {
226
+ 'cache-control': 'public, max-age=120',
227
+ date: 'Mon, Feb 27 2023 12:08:36 GMT',
228
+ expires: 'Mon, Feb 27 2023 12:10:36 GMT',
229
+ server: 'Upstream 1.2',
230
+ vary: 'Accept-Language',
231
+ })
232
+ )
233
+
234
+ // unconditional GET
235
+ let res = await app.request('http://localhost/etag/ghi')
236
+ expect(res.status).toBe(200)
237
+ expect(res.headers.get('ETag')).not.toBeFalsy()
238
+ const etagHeaderValue = res.headers.get('ETag') || ''
239
+
240
+ // conditional GET with the wrong ETag:
241
+ res = await app.request('http://localhost/etag/ghi', {
242
+ headers: {
243
+ 'If-None-Match': '"not the right etag"',
244
+ },
245
+ })
246
+ expect(res.status).toBe(200)
247
+
248
+ // conditional GET with matching ETag:
249
+ res = await app.request('http://localhost/etag/ghi', {
250
+ headers: {
251
+ 'If-None-Match': etagHeaderValue,
252
+ },
253
+ })
254
+ expect(res.status).toBe(304)
255
+ expect(res.headers.get('Etag')).toBe(etagHeaderValue)
256
+ expect(await res.text()).toBe('')
257
+ expect(res.headers.get('cache-control')).toBe('public, max-age=120')
258
+ expect(res.headers.get('date')).toBe('Mon, Feb 27 2023 12:08:36 GMT')
259
+ expect(res.headers.get('expires')).toBe('Mon, Feb 27 2023 12:10:36 GMT')
260
+ expect(res.headers.get('server')).toBeFalsy()
261
+ expect(res.headers.get('vary')).toBe('Accept-Language')
262
+
263
+ // conditional GET with matching ETag among list:
264
+ res = await app.request('http://localhost/etag/ghi', {
265
+ headers: {
266
+ 'If-None-Match': `"mismatch 1", ${etagHeaderValue}, "mismatch 2"`,
267
+ },
268
+ })
269
+ expect(res.status).toBe(304)
270
+
271
+ // conditional GET with matching ETag among list, OWS before the comma (RFC 9110):
272
+ res = await app.request('http://localhost/etag/ghi', {
273
+ headers: {
274
+ 'If-None-Match': `"mismatch 1", ${etagHeaderValue} , "mismatch 2"`,
275
+ },
276
+ })
277
+ expect(res.status).toBe(304)
278
+
279
+ // conditional GET with `*` wildcard:
280
+ res = await app.request('http://localhost/etag/ghi', {
281
+ headers: {
282
+ 'If-None-Match': '*',
283
+ },
284
+ })
285
+ expect(res.status).toBe(304)
286
+ })
287
+
288
+ it('Should handle conditional QUERY requests', async () => {
289
+ const app = new App()
290
+ app.use('/etag/*', etag())
291
+ app.on('QUERY', '/etag/query', (c) => c.text('App is great'))
292
+
293
+ // unconditional QUERY
294
+ let res = await app.request('http://localhost/etag/query', {
295
+ method: 'QUERY',
296
+ body: 'select *',
297
+ })
298
+ expect(res.status).toBe(200)
299
+ expect(res.headers.get('ETag')).not.toBeFalsy()
300
+ const etagHeaderValue = res.headers.get('ETag') || ''
301
+
302
+ // conditional QUERY with the wrong ETag:
303
+ res = await app.request('http://localhost/etag/query', {
304
+ method: 'QUERY',
305
+ body: 'select *',
306
+ headers: {
307
+ 'If-None-Match': '"not the right etag"',
308
+ },
309
+ })
310
+ expect(res.status).toBe(200)
311
+
312
+ // conditional QUERY with matching ETag:
313
+ res = await app.request('http://localhost/etag/query', {
314
+ method: 'QUERY',
315
+ body: 'select *',
316
+ headers: {
317
+ 'If-None-Match': etagHeaderValue,
318
+ },
319
+ })
320
+ expect(res.status).toBe(304)
321
+ expect(res.headers.get('ETag')).toBe(etagHeaderValue)
322
+ expect(await res.text()).toBe('')
323
+
324
+ // conditional QUERY with `*` wildcard:
325
+ res = await app.request('http://localhost/etag/query', {
326
+ method: 'QUERY',
327
+ body: 'select *',
328
+ headers: {
329
+ 'If-None-Match': '*',
330
+ },
331
+ })
332
+ expect(res.status).toBe(304)
333
+ })
334
+
335
+ it('Should not return 304 on unsafe methods or error responses', async () => {
336
+ const app = new App()
337
+ app.use('/etag/*', etag())
338
+ app.put('/etag/put', (c) => c.text('created', 201, { ETag: '"etag-123"' }))
339
+ app.get('/etag/not-found', (c) => c.text('not found', 404, { ETag: '"etag-123"' }))
340
+
341
+ // Unsafe methods:
342
+ let res = await app.request('http://localhost/etag/put', {
343
+ method: 'PUT',
344
+ headers: {
345
+ 'If-None-Match': '*',
346
+ },
347
+ })
348
+ expect(res.status).toBe(201)
349
+ expect(await res.text()).toBe('created')
350
+
351
+ res = await app.request('http://localhost/etag/put', {
352
+ method: 'PUT',
353
+ headers: {
354
+ 'If-None-Match': '"etag-123"',
355
+ },
356
+ })
357
+ expect(res.status).toBe(201)
358
+ expect(await res.text()).toBe('created')
359
+
360
+ // Error responses:
361
+ res = await app.request('http://localhost/etag/not-found', {
362
+ headers: {
363
+ 'If-None-Match': '*',
364
+ },
365
+ })
366
+ expect(res.status).toBe(404)
367
+
368
+ res = await app.request('http://localhost/etag/not-found', {
369
+ headers: {
370
+ 'If-None-Match': '"etag-123"',
371
+ },
372
+ })
373
+ expect(res.status).toBe(404)
374
+ })
375
+
376
+ it('Should not return duplicate etag header values', async () => {
377
+ const app = new App()
378
+ app.use('/etag/*', etag())
379
+ app.use('/etag/*', etag())
380
+ app.get('/etag/abc', (c) => c.text('App is hot'))
381
+
382
+ const res = await app.request('http://localhost/etag/abc')
383
+ expect(res.headers.get('ETag')).not.toBeFalsy()
384
+ expect(res.headers.get('ETag')).toBe('"7c6c452b40ba15d4748e0075f4e402b441c3ea15"')
385
+ })
386
+
387
+ it('Should not override ETag headers from upstream', async () => {
388
+ const app = new App()
389
+ app.use('/etag/*', etag())
390
+ app.get('/etag/predefined', (c) =>
391
+ c.text('This response has an ETag', 200, { ETag: '"f-0194-d"' })
392
+ )
393
+
394
+ const res = await app.request('http://localhost/etag/predefined')
395
+ expect(res.headers.get('ETag')).toBe('"f-0194-d"')
396
+ })
397
+
398
+ it('Should retain the default and the specified headers', async () => {
399
+ const cacheControl = 'public, max-age=120'
400
+ const message = 'Hello!'
401
+ const app = new App()
402
+ app.use(
403
+ '/etag/*',
404
+ etag({
405
+ retainedHeaders: ['x-message-retain', 'X-Message-Retain-Upper', ...RETAINED_304_HEADERS],
406
+ })
407
+ )
408
+ app.get('/etag', (c) => {
409
+ return c.text('App is hot', 200, {
410
+ 'cache-control': cacheControl,
411
+ 'x-message-retain': message,
412
+ 'X-Message-Retain-Upper': message,
413
+ 'x-message': message,
414
+ })
415
+ })
416
+ const res = await app.request('/etag', {
417
+ headers: {
418
+ 'If-None-Match': '"7c6c452b40ba15d4748e0075f4e402b441c3ea15"',
419
+ },
420
+ })
421
+ expect(res.status).toBe(304)
422
+ expect(res.headers.get('ETag')).not.toBeFalsy()
423
+ expect(res.headers.get('ETag')).toBe('"7c6c452b40ba15d4748e0075f4e402b441c3ea15"')
424
+ expect(res.headers.get('Cache-Control')).toBe(cacheControl)
425
+ expect(res.headers.get('x-message-retain')).toBe(message)
426
+ expect(res.headers.get('X-Message-Retain-Upper')).toBe(message)
427
+ expect(res.headers.get('x-message')).toBeFalsy()
428
+ })
429
+
430
+ it('Should return 304 when weak ETag in If-None-Match matches the generated ETag', async () => {
431
+ const app = new App()
432
+ app.use('/etag/*', etag())
433
+ app.get('/etag/abc', (c) => {
434
+ return c.text('App is hot')
435
+ })
436
+ let res = await app.request('http://localhost/etag/abc')
437
+ const headerEtag = res.headers.get('ETag')!
438
+
439
+ expect(headerEtag).not.toBeFalsy()
440
+
441
+ res = await app.request('http://localhost/etag/abc', {
442
+ headers: {
443
+ 'If-None-Match': 'W/"7c6c452b40ba15d4748e0075f4e402b441c3ea15"',
444
+ },
445
+ })
446
+
447
+ expect(res.status).toBe(304)
448
+ })
449
+
450
+ describe('When crypto is not available', () => {
451
+ let _crypto: Crypto | undefined
452
+ beforeAll(() => {
453
+ _crypto = globalThis.crypto
454
+ Object.defineProperty(globalThis, 'crypto', {
455
+ value: {},
456
+ })
457
+ })
458
+
459
+ afterAll(() => {
460
+ Object.defineProperty(globalThis, 'crypto', {
461
+ value: _crypto,
462
+ })
463
+ })
464
+
465
+ it('Should not generate etag', async () => {
466
+ const app = new App()
467
+ app.use('/etag/*', etag())
468
+ app.get('/etag/no-digest', (c) => c.text('App is hot'))
469
+ const res = await app.request('/etag/no-digest')
470
+ expect(res.status).toBe(200)
471
+ expect(res.headers.get('ETag')).toBeNull()
472
+ })
473
+ })
474
+
475
+ it('Should remove all non-retained headers on 304 without skipping any', async () => {
476
+ const app = new App()
477
+ app.use('/etag/*', etag())
478
+ app.get('/etag/multi-headers', (c) => {
479
+ c.header('X-Custom-1', 'val1')
480
+ c.header('X-Custom-2', 'val2')
481
+ c.header('X-Custom-3', 'val3')
482
+ c.header('X-Custom-4', 'val4')
483
+ c.header('Cache-Control', 'max-age=3600')
484
+ return c.text('App is hot')
485
+ })
486
+ const res1 = await app.request('http://localhost/etag/multi-headers')
487
+ const etagHeader = res1.headers.get('ETag')!
488
+ expect(res1.status).toBe(200)
489
+
490
+ const res2 = await app.request('http://localhost/etag/multi-headers', {
491
+ headers: {
492
+ 'If-None-Match': etagHeader,
493
+ },
494
+ })
495
+ expect(res2.status).toBe(304)
496
+ expect(res2.headers.get('ETag')).toBe(etagHeader)
497
+ expect(res2.headers.get('Cache-Control')).toBe('max-age=3600')
498
+ expect(res2.headers.get('X-Custom-1')).toBeNull()
499
+ expect(res2.headers.get('X-Custom-2')).toBeNull()
500
+ expect(res2.headers.get('X-Custom-3')).toBeNull()
501
+ expect(res2.headers.get('X-Custom-4')).toBeNull()
502
+ })
503
+ })
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @module
3
+ * ETag Middleware for App.
4
+ */
5
+
6
+ import type { MiddlewareHandler } from '../../types.ts'
7
+ import { generateDigest } from './digest.ts'
8
+
9
+ type ETagOptions = {
10
+ retainedHeaders?: string[]
11
+ weak?: boolean
12
+ generateDigest?: (body: Uint8Array<ArrayBuffer>) => ArrayBuffer | Promise<ArrayBuffer>
13
+ }
14
+
15
+ /**
16
+ * Default headers to pass through on 304 responses. From the spec:
17
+ * > The response must not contain a body and must include the headers that
18
+ * > would have been sent in an equivalent 200 OK response: Cache-Control,
19
+ * > Content-Location, Date, ETag, Expires, and Vary.
20
+ */
21
+ export const RETAINED_304_HEADERS = [
22
+ 'cache-control',
23
+ 'content-location',
24
+ 'date',
25
+ 'etag',
26
+ 'expires',
27
+ 'vary',
28
+ ]
29
+
30
+ const stripWeak = (tag: string) => tag.replace(/^W\//, '')
31
+
32
+ function etagMatches(etag: string, ifNoneMatch: string | null) {
33
+ return (
34
+ ifNoneMatch != null &&
35
+ ifNoneMatch.split(',').some((t) => stripWeak(t.trim()) === stripWeak(etag))
36
+ )
37
+ }
38
+
39
+ function initializeGenerator(
40
+ generator?: ETagOptions['generateDigest']
41
+ ): ETagOptions['generateDigest'] | undefined {
42
+ if (!generator) {
43
+ if (crypto && crypto.subtle) {
44
+ generator = (body: Uint8Array<ArrayBuffer>) =>
45
+ crypto.subtle.digest(
46
+ {
47
+ name: 'SHA-1',
48
+ },
49
+ body
50
+ )
51
+ }
52
+ }
53
+
54
+ return generator
55
+ }
56
+
57
+ /**
58
+ * ETag Middleware for App.
59
+ *
60
+ *
61
+ * @param {ETagOptions} [options] - The options for the ETag middleware.
62
+ * @param {boolean} [options.weak=false] - Define using or not using a weak validation. If true is set, then `W/` is added to the prefix of the value.
63
+ * @param {string[]} [options.retainedHeaders=RETAINED_304_HEADERS] - The headers that you want to retain in the 304 Response.
64
+ * @param {function(Uint8Array): ArrayBuffer | Promise<ArrayBuffer>} [options.generateDigest] -
65
+ * A custom digest generation function. By default, it uses 'SHA-1'
66
+ * This function is called with the response body as a `Uint8Array` and should return a hash as an `ArrayBuffer` or a Promise of one.
67
+ * @returns {MiddlewareHandler} The middleware handler function.
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const app = new App()
72
+ *
73
+ * app.use('/etag/*', etag())
74
+ * app.get('/etag/abc', (c) => {
75
+ * return c.text('App is hot')
76
+ * })
77
+ * ```
78
+ */
79
+ export const etag = (options?: ETagOptions): MiddlewareHandler => {
80
+ const retainedHeaders = new Set(
81
+ (options?.retainedHeaders ?? RETAINED_304_HEADERS).map((header) => header.toLowerCase())
82
+ )
83
+ const weak = options?.weak ?? false
84
+ const generator = initializeGenerator(options?.generateDigest)
85
+
86
+ return async function etag(c, next) {
87
+ const ifNoneMatch = c.req.header('If-None-Match') ?? null
88
+
89
+ await next()
90
+
91
+ if (
92
+ !(c.req.method === 'GET' || c.req.method === 'HEAD' || c.req.method === 'QUERY') ||
93
+ !c.res.ok
94
+ ) {
95
+ return
96
+ }
97
+
98
+ const res = c.res as Response
99
+ let etag = res.headers.get('ETag')
100
+
101
+ if (!etag) {
102
+ if (!generator) {
103
+ return
104
+ }
105
+ const hash = await generateDigest(
106
+ // This type casing avoids the type error for `deno publish`
107
+ res.clone().body as ReadableStream<Uint8Array<ArrayBuffer>>,
108
+ generator
109
+ )
110
+ if (hash === null) {
111
+ return
112
+ }
113
+ etag = weak ? `W/"${hash}"` : `"${hash}"`
114
+ }
115
+
116
+ const matched = ifNoneMatch === '*' || etagMatches(etag, ifNoneMatch)
117
+
118
+ if (matched) {
119
+ c.res = new Response(null, {
120
+ status: 304,
121
+ statusText: 'Not Modified',
122
+ headers: {
123
+ ETag: etag,
124
+ },
125
+ })
126
+ for (const key of Array.from(c.res.headers.keys())) {
127
+ if (!retainedHeaders.has(key.toLowerCase())) {
128
+ c.res.headers.delete(key)
129
+ }
130
+ }
131
+ } else {
132
+ c.res.headers.set('ETag', etag)
133
+ }
134
+ }
135
+ }