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,409 @@
1
+ /**
2
+ * The router as a Cordis service: mounts owned by the plugin that made them, and the lifecycle mechanics the repo protocol requires -
3
+ * PENDING while the provider is missing, reactivation when it appears, provider replacement without duplicate registrations, disposal,
4
+ * repeated mount and reload, in-flight abort, and streams that end with the router or with the plugin that serves them. Routing itself is covered by the core's own suite.
5
+ */
6
+
7
+ import { Context, type FiberState } from '@deepseek-ai/cordis'
8
+ import { afterEach, describe, expect, it } from 'vitest'
9
+ import { streamSSE } from '../src/helpers/index.ts'
10
+ import { apply, App, Config, connectionOf, HTTPException, name, type Router } from '../src/index.ts'
11
+
12
+ const PENDING = 0 as FiberState.PENDING
13
+ const ACTIVE = 2 as FiberState.ACTIVE
14
+ const DISPOSED = 4 as FiberState.DISPOSED
15
+
16
+ const contexts: Context[] = []
17
+ const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
+
19
+ function root(): Context {
20
+ const ctx = new Context()
21
+ contexts.push(ctx)
22
+ return ctx
23
+ }
24
+
25
+ const provider = { name, apply }
26
+ const config: Config = { trailingSlash: 'strict', disposeGraceMs: 2000 }
27
+
28
+ async function mountRouter(): Promise<{ ctx: Context, router: Router }> {
29
+ const ctx = root()
30
+ ctx.plugin(provider, config)
31
+ await settle()
32
+ return { ctx, router: ctx.router }
33
+ }
34
+
35
+ afterEach(async () => {
36
+ while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
37
+ })
38
+
39
+ /** A consumer plugin that needs `router` and mounts one route, the way the Market does. */
40
+ function consumer(path: string, label: string) {
41
+ return {
42
+ name: `consumer${label}`,
43
+ inject: ['router'],
44
+ apply(ctx: Context): void {
45
+ ctx.router.route('/', new App().get(path, c => c.text(label)))
46
+ },
47
+ }
48
+ }
49
+
50
+ const get = (router: Router, path: string): Promise<Response> => router.fetch(new Request(`http://acryl.test${path}`))
51
+
52
+ describe('acryl-router mounts', () => {
53
+ it('serves a mounted app under its path, with the app\'s own routes, middleware and error handler', async () => {
54
+ const { router } = await mountRouter()
55
+ const api = new App()
56
+ .use(async (c, next) => { await next(); c.header('x-api', 'yes') })
57
+ .get('/items/:id', c => c.json({ id: c.req.param('id') }))
58
+ .get('/boom', () => { throw new Error('boom') })
59
+ .onError((error, c) => c.json({ handled: error instanceof Error ? error.message : 'unknown' }, 418))
60
+ router.route('/api', api)
61
+
62
+ const item = await get(router, '/api/items/a%20b')
63
+ expect(await item.json()).toEqual({ id: 'a b' })
64
+ expect(item.headers.get('x-api')).toBe('yes')
65
+ const boom = await get(router, '/api/boom')
66
+ expect(boom.status).toBe(418)
67
+ expect(await boom.json()).toEqual({ handled: 'boom' })
68
+ expect((await get(router, '/items/a')).status).toBe(404)
69
+ })
70
+
71
+ it('removes a mount with its remover, idempotently, and keeps the others', async () => {
72
+ const { router } = await mountRouter()
73
+ const removeA = router.route('/', new App().get('/a', c => c.text('a')))
74
+ router.route('/', new App().get('/b', c => c.text('b')))
75
+ expect((await get(router, '/a')).status).toBe(200)
76
+ removeA()
77
+ removeA()
78
+ expect((await get(router, '/a')).status).toBe(404)
79
+ expect(await (await get(router, '/b')).text()).toBe('b')
80
+ router.route('/', new App().get('/a', c => c.text('a2')))
81
+ expect(await (await get(router, '/a')).text()).toBe('a2')
82
+ })
83
+
84
+ it('runs mounts in the order they were made, so an earlier mount\'s middleware wraps a later mount\'s routes', async () => {
85
+ const { router } = await mountRouter()
86
+ router.route('/', new App().use('/guarded/*', async c => c.text('denied', 401)))
87
+ router.route('/', new App().get('/guarded/x', c => c.text('secret')).get('/open', c => c.text('open')))
88
+ expect((await get(router, '/guarded/x')).status).toBe(401)
89
+ expect(await (await get(router, '/open')).text()).toBe('open')
90
+ })
91
+
92
+ it('takes one router-wide error handler and one not-found handler, owned by the caller', async () => {
93
+ const { router } = await mountRouter()
94
+ router.route('/', new App().get('/boom', () => { throw new Error('boom') }).get('/teapot', () => { throw new HTTPException(418, { message: 'short' }) }))
95
+ expect((await get(router, '/boom')).status).toBe(500)
96
+ expect((await get(router, '/teapot')).status).toBe(418)
97
+
98
+ const removeError = router.onError((_error, c) => c.text('custom', 503))
99
+ const removeNotFound = router.notFound(c => c.text('nothing here', 404))
100
+ expect(() => router.onError((_error, c) => c.text('second'))).toThrow(/already registered/)
101
+ expect((await get(router, '/boom')).status).toBe(503)
102
+ expect(await (await get(router, '/missing')).text()).toBe('nothing here')
103
+ removeError()
104
+ removeNotFound()
105
+ expect((await get(router, '/boom')).status).toBe(500)
106
+ })
107
+
108
+ it('treats /a and /a/ as one path when trailingSlash is ignore', async () => {
109
+ const ctx = root()
110
+ ctx.plugin(provider, { trailingSlash: 'ignore', disposeGraceMs: 2000 })
111
+ await settle()
112
+ ctx.router.route('/', new App().get('/a', c => c.text('a')))
113
+ expect((await get(ctx.router, '/a/')).status).toBe(200)
114
+ })
115
+
116
+ it('hands the transport\'s connection info to the handler and gives an in-process call an empty one', async () => {
117
+ const { router } = await mountRouter()
118
+ router.route('/', new App().get('/who', c => c.json({ remote: connectionOf(c).remoteAddress ?? null, port: connectionOf(c).localPort ?? null })))
119
+ const over = await router.fetch(new Request('http://acryl.test/who'), { remoteAddress: '127.0.0.1', localPort: 4000 })
120
+ expect(await over.json()).toEqual({ remote: '127.0.0.1', port: 4000 })
121
+ const inProcess = await router.request('/who')
122
+ expect(await inProcess.json()).toEqual({ remote: null, port: null })
123
+ })
124
+ })
125
+
126
+ describe('acryl-router lifecycle through Cordis', () => {
127
+ it('leaves a consumer PENDING while router is missing and activates it when router appears', async () => {
128
+ const ctx = root()
129
+ const fiber = ctx.plugin(consumer('/c', 'A'))
130
+ await settle()
131
+ expect(fiber.state).toBe(PENDING)
132
+
133
+ ctx.plugin(provider, config)
134
+ await settle()
135
+ expect(fiber.state).toBe(ACTIVE)
136
+ expect(await (await get(ctx.router, '/c')).text()).toBe('A')
137
+ expect(ctx.router.size).toBe(1)
138
+ })
139
+
140
+ it('removes a consumer\'s mounts when the consumer unloads and leaves other consumers alone', async () => {
141
+ const { ctx, router } = await mountRouter()
142
+ const a = ctx.plugin(consumer('/a', 'A'))
143
+ ctx.plugin(consumer('/b', 'B'))
144
+ await settle()
145
+ expect(router.size).toBe(2)
146
+
147
+ await a.dispose()
148
+ await settle()
149
+ expect(a.state).toBe(DISPOSED)
150
+ expect((await get(router, '/a')).status).toBe(404)
151
+ expect(await (await get(router, '/b')).text()).toBe('B')
152
+ expect(router.size).toBe(1)
153
+ })
154
+
155
+ it('replaces the provider without leaving doubled registrations: consumers re-register exactly once', async () => {
156
+ const ctx = root()
157
+ const first = ctx.plugin(provider, config)
158
+ const fiber = ctx.plugin(consumer('/c', 'A'))
159
+ await settle()
160
+ const oldRouter = ctx.router
161
+ expect(fiber.state).toBe(ACTIVE)
162
+
163
+ await first.dispose()
164
+ await settle()
165
+ expect(fiber.state).toBe(PENDING)
166
+ expect(oldRouter.size).toBe(0)
167
+
168
+ ctx.plugin(provider, config)
169
+ await settle()
170
+ expect(fiber.state).toBe(ACTIVE)
171
+ expect(ctx.router).not.toBe(oldRouter)
172
+ expect(ctx.router.size).toBe(1)
173
+ expect(await (await get(ctx.router, '/c')).text()).toBe('A')
174
+ })
175
+
176
+ it('serves the routes an app had when it was mounted, whatever else changes later', async () => {
177
+ const { ctx, router } = await mountRouter()
178
+ const app = new App().get('/early', c => c.text('early'))
179
+ router.route('/', app)
180
+ app.get('/late', c => c.text('late'))
181
+ expect((await get(router, '/early')).status).toBe(200)
182
+ expect((await get(router, '/late')).status).toBe(404)
183
+ // Another plugin's mount recomposes the router; the late route still is not served.
184
+ ctx.plugin(consumer('/other', 'other'))
185
+ await settle()
186
+ expect((await get(router, '/other')).status).toBe(200)
187
+ expect((await get(router, '/late')).status).toBe(404)
188
+ })
189
+
190
+ it('serves two plugins on the same path in mount order, and hands the path to the other when the first unloads', async () => {
191
+ const { ctx, router } = await mountRouter()
192
+ const first = ctx.plugin(consumer('/same', 'first'))
193
+ await settle()
194
+ ctx.plugin(consumer('/same', 'second'))
195
+ await settle()
196
+ expect(await (await get(router, '/same')).text()).toBe('first')
197
+ await first.dispose()
198
+ await settle()
199
+ expect(await (await get(router, '/same')).text()).toBe('second')
200
+ })
201
+
202
+ it('runs a reloaded plugin\'s handler once per request: reloading leaves no duplicate route behind, ten times', async () => {
203
+ const { ctx, router } = await mountRouter()
204
+ let calls = 0
205
+ const counting = {
206
+ name: 'counting',
207
+ inject: ['router'],
208
+ apply(inner: Context): void {
209
+ inner.router.route('/', new App().use(async (_c, next) => { calls += 1; await next() }).get('/count', c => c.text(String(calls))))
210
+ },
211
+ }
212
+ for (let round = 0; round < 10; round += 1) {
213
+ const fiber = ctx.plugin(counting)
214
+ await settle()
215
+ expect(router.size).toBe(1)
216
+ calls = 0
217
+ expect(await (await get(router, '/count')).text()).toBe('1')
218
+ await fiber.dispose()
219
+ await settle()
220
+ expect(router.size).toBe(0)
221
+ }
222
+ })
223
+
224
+ it('returns to its baseline after fifty consumer mount and unload cycles', async () => {
225
+ const { ctx, router } = await mountRouter()
226
+ ctx.plugin(consumer('/stay', 'S'))
227
+ await settle()
228
+ const baseline = router.size
229
+ for (let i = 0; i < 50; i += 1) {
230
+ const fiber = ctx.plugin(consumer(`/cycle/${String(i)}`, `C${String(i)}`))
231
+ await settle()
232
+ expect(router.size).toBe(baseline + 1)
233
+ expect(await (await get(router, `/cycle/${String(i)}`)).text()).toBe(`C${String(i)}`)
234
+ await fiber.dispose()
235
+ await settle()
236
+ }
237
+ expect(router.size).toBe(baseline)
238
+ expect((await get(router, '/cycle/0')).status).toBe(404)
239
+ })
240
+
241
+ it('aborts an in-flight request and waits for it when the router is disposed, then answers 503', async () => {
242
+ const ctx = root()
243
+ const fiber = ctx.plugin(provider, config)
244
+ await settle()
245
+ const router = ctx.router
246
+ let aborted = false
247
+ router.route('/', new App().get('/slow', async c => {
248
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { aborted = true; reject(new Error('aborted')) }, { once: true }) })
249
+ return c.text('late')
250
+ }))
251
+ const pending = get(router, '/slow')
252
+ await settle()
253
+
254
+ await fiber.dispose()
255
+ await settle()
256
+ expect(aborted).toBe(true)
257
+ expect((await pending).status).toBe(499)
258
+ expect((await get(router, '/slow')).status).toBe(503)
259
+ })
260
+
261
+ it('forwards the caller\'s own abort to the handler signal', async () => {
262
+ const { router } = await mountRouter()
263
+ let seen = false
264
+ router.route('/', new App().get('/slow', async c => {
265
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { seen = true; reject(new Error('aborted')) }, { once: true }) })
266
+ return c.text('x')
267
+ }))
268
+ const controller = new AbortController()
269
+ const pending = router.fetch(new Request('http://acryl.test/slow', { signal: controller.signal }))
270
+ await settle()
271
+ controller.abort()
272
+ expect((await pending).status).toBe(499)
273
+ expect(seen).toBe(true)
274
+ })
275
+
276
+ it('ends a server-sent-event stream when the router is disposed: the stream\'s abort cleanup runs and the reader sees the end', async () => {
277
+ const ctx = root()
278
+ const fiber = ctx.plugin(provider, config)
279
+ await settle()
280
+ let cleaned = false
281
+ ctx.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
282
+ stream.onAbort(() => { cleaned = true })
283
+ await stream.writeSSE({ data: 'first' })
284
+ while (!stream.aborted) await stream.sleep(10)
285
+ })))
286
+ const response = await get(ctx.router, '/events')
287
+ expect(response.headers.get('content-type')).toBe('text/event-stream')
288
+ const reader = response.body!.getReader()
289
+ expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
290
+
291
+ await fiber.dispose()
292
+ expect(cleaned).toBe(true)
293
+ await expect(reader.read()).resolves.toMatchObject({ done: true })
294
+ })
295
+
296
+ it('ends the stream a plugin opened when that plugin unloads, and leaves another plugin\'s request running', async () => {
297
+ const { ctx, router } = await mountRouter()
298
+ let cleaned = false
299
+ let otherAborted = false
300
+ let release!: () => void
301
+ const streaming = ctx.plugin({
302
+ name: 'streaming',
303
+ inject: ['router'],
304
+ apply(inner: Context): void {
305
+ inner.router.route('/', new App().get('/events', c => streamSSE(c, async stream => {
306
+ stream.onAbort(() => { cleaned = true })
307
+ await stream.writeSSE({ data: 'first' })
308
+ while (!stream.aborted) await stream.sleep(10)
309
+ })))
310
+ },
311
+ })
312
+ ctx.plugin({
313
+ name: 'other',
314
+ inject: ['router'],
315
+ apply(inner: Context): void {
316
+ inner.router.route('/', new App().get('/slow', async c => {
317
+ c.req.raw.signal.addEventListener('abort', () => { otherAborted = true }, { once: true })
318
+ await new Promise<void>(resolve => { release = resolve })
319
+ return c.text('done')
320
+ }))
321
+ },
322
+ })
323
+ await settle()
324
+ const response = await get(router, '/events')
325
+ const reader = response.body!.getReader()
326
+ expect(new TextDecoder().decode((await reader.read()).value)).toBe('data: first\n\n')
327
+ const slow = get(router, '/slow')
328
+ await settle()
329
+
330
+ await streaming.dispose()
331
+ expect(cleaned).toBe(true)
332
+ await expect(reader.read()).resolves.toMatchObject({ done: true })
333
+ expect(otherAborted).toBe(false)
334
+ release()
335
+ expect(await (await slow).text()).toBe('done')
336
+ })
337
+
338
+ it('aborts a request still inside an unloading plugin\'s middleware, but not one that middleware already passed on', async () => {
339
+ const { ctx, router } = await mountRouter()
340
+ let guardAborted = false
341
+ let handlerAborted = false
342
+ let release!: () => void
343
+ const guard = ctx.plugin({
344
+ name: 'guard',
345
+ inject: ['router'],
346
+ apply(inner: Context): void {
347
+ inner.router.route('/', new App()
348
+ .use('/held/*', async (c, next) => {
349
+ c.req.raw.signal.addEventListener('abort', () => { guardAborted = true }, { once: true })
350
+ await new Promise<void>((_resolve, reject) => { c.req.raw.signal.addEventListener('abort', () => { reject(new Error('aborted')) }, { once: true }) })
351
+ await next()
352
+ })
353
+ .use('/passed/*', async (_c, next) => { await next() }))
354
+ },
355
+ })
356
+ ctx.plugin({
357
+ name: 'handler',
358
+ inject: ['router'],
359
+ apply(inner: Context): void {
360
+ inner.router.route('/', new App()
361
+ .get('/held/x', c => c.text('held'))
362
+ .get('/passed/x', c => streamSSE(c, async stream => {
363
+ stream.onAbort(() => { handlerAborted = true })
364
+ await stream.writeSSE({ data: 'open' })
365
+ await new Promise<void>(resolve => { release = resolve })
366
+ })))
367
+ },
368
+ })
369
+ await settle()
370
+ const held = get(router, '/held/x')
371
+ const passed = await get(router, '/passed/x')
372
+ const reader = passed.body!.getReader()
373
+ await reader.read()
374
+
375
+ await guard.dispose()
376
+ expect(guardAborted).toBe(true)
377
+ expect((await held).status).toBe(499)
378
+ expect(handlerAborted).toBe(false)
379
+ release()
380
+ await expect(reader.read()).resolves.toMatchObject({ done: true })
381
+ })
382
+
383
+ it('keeps a request in flight through waitUntil until its task settles', async () => {
384
+ const ctx = root()
385
+ const fiber = ctx.plugin(provider, config)
386
+ await settle()
387
+ let finished = false
388
+ ctx.router.route('/', new App().get('/later', c => {
389
+ c.executionCtx.waitUntil(new Promise<void>(resolve => { setTimeout(() => { finished = true; resolve() }, 30) }))
390
+ return c.text('now')
391
+ }))
392
+ expect(await (await get(ctx.router, '/later')).text()).toBe('now')
393
+ expect(finished).toBe(false)
394
+ await fiber.dispose()
395
+ expect(finished).toBe(true)
396
+ })
397
+
398
+ it('gives up on a request that ignores the abort after the grace period instead of hanging disposal', async () => {
399
+ const ctx = root()
400
+ const fiber = ctx.plugin(provider, { trailingSlash: 'strict', disposeGraceMs: 30 })
401
+ await settle()
402
+ ctx.router.route('/', new App().get('/stuck', c => { c.executionCtx.waitUntil(new Promise(() => undefined)); return c.text('x') }))
403
+ await get(ctx.router, '/stuck')
404
+ const started = Date.now()
405
+ await fiber.dispose()
406
+ expect(Date.now() - started).toBeLessThan(1000)
407
+ expect(fiber.state).toBe(DISPOSED)
408
+ })
409
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * What a Service's fields look like to the plugins that call it. A plugin reaches a service through its own shadow of the instance
3
+ * (`ctx.counter` in plugin A and in plugin B are two different objects over one service), but reads and writes of fields pass through to
4
+ * the one instance: a field assigned in a method is seen by every caller, as is state kept in an object that is only mutated. What the
5
+ * shadow does change is `this.ctx` inside a method, which `live-install.spec.ts` covers.
6
+ */
7
+ import { Context, Service } from '@deepseek-ai/cordis'
8
+ import { afterEach, describe, expect, it } from 'vitest'
9
+
10
+ declare module '@deepseek-ai/cordis' {
11
+ interface Context {
12
+ counter: Counter
13
+ }
14
+ }
15
+
16
+ const contexts: Context[] = []
17
+ const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0))
18
+ afterEach(async () => {
19
+ while (contexts.length > 0) await contexts.pop()?.fiber.dispose()
20
+ })
21
+
22
+ class Counter extends Service {
23
+ /** Reassigned by `assign`. */
24
+ assigned = 0
25
+ /** Created once, only mutated. */
26
+ private readonly state = { count: 0 }
27
+
28
+ constructor(ctx: Context) {
29
+ super(ctx, 'counter')
30
+ }
31
+
32
+ assign(): void {
33
+ this.assigned += 1
34
+ }
35
+
36
+ mutate(): void {
37
+ this.state.count += 1
38
+ }
39
+
40
+ get count(): number {
41
+ return this.state.count
42
+ }
43
+ }
44
+
45
+ async function twoCallers(): Promise<{ a: Context, b: Context }> {
46
+ const root = new Context()
47
+ contexts.push(root)
48
+ root.plugin({ name: 'counter', apply: (ctx: Context) => { new Counter(ctx) } })
49
+ const callers: Context[] = []
50
+ for (const name of ['a', 'b']) root.plugin({ name, inject: ['counter'], apply: (ctx: Context) => { callers.push(ctx) } })
51
+ await settle()
52
+ const [a, b] = callers
53
+ if (a === undefined || b === undefined) throw new Error('both callers should be active')
54
+ return { a, b }
55
+ }
56
+
57
+ describe('service state and the shadow context', () => {
58
+ it('shows a field assigned in a method through one caller to every caller', async () => {
59
+ const { a, b } = await twoCallers()
60
+ a.counter.assign()
61
+ b.counter.assign()
62
+ expect(a.counter.assigned).toBe(2)
63
+ expect(b.counter.assigned).toBe(2)
64
+ })
65
+
66
+ it('shows a field assigned from outside through one caller to every caller', async () => {
67
+ const { a, b } = await twoCallers()
68
+ a.counter.assigned = 5
69
+ expect(b.counter.assigned).toBe(5)
70
+ })
71
+
72
+ it('shares state kept in one object that is only mutated', async () => {
73
+ const { a, b } = await twoCallers()
74
+ a.counter.mutate()
75
+ b.counter.mutate()
76
+ expect(a.counter.count).toBe(2)
77
+ expect(b.counter.count).toBe(2)
78
+ })
79
+ })
@@ -0,0 +1,20 @@
1
+ {
2
+ "source": {
3
+ "repo": "acryldev/acryl",
4
+ "path": "plugins/acryl-router",
5
+ "commit": "df79b188398b6d4f8cceda35a72b88300a10a9ce"
6
+ },
7
+ "vendored": [
8
+ "src",
9
+ "tests",
10
+ "LICENSE"
11
+ ],
12
+ "rewrittenSpecifiers": 0,
13
+ "digest": "sha256:6e15f8ce0499b94c95649342f6fe984bc9b2aec0f5a142e8853a5badcc3b85f5",
14
+ "testsNotRun": {
15
+ "tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
16
+ },
17
+ "notice": [
18
+ "LICENSE"
19
+ ]
20
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebBoxes s.r.o.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "acryl-router-http",
3
+ "description": "The serve() and request bridge that acryl-node-server uses to put the router on node:http. Mounting it as a plugin needs the DSH web server, which a generated app does not have. Vendored from ACRYL.",
4
+ "requires": ["acryl-router"],
5
+ "inject": ["router"],
6
+ "provides": [],
7
+ "dependencies": {
8
+ "@deepseek-ai/cordis": "4.0.4"
9
+ },
10
+ "mount": "not mounted: it is the library behind acryl-node-server",
11
+ "omit": ["**/*.test.*", "**/*.case.ts"]
12
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The two conversions between `node:http` and Web-standard fetch objects. Pure functions, no Cordis, so the adapter's
3
+ * behavior is testable without a server.
4
+ */
5
+
6
+ import type { IncomingMessage, ServerResponse } from 'node:http'
7
+ import { Readable } from 'node:stream'
8
+ import { pipeline } from 'node:stream/promises'
9
+
10
+ const FALLBACK_ORIGIN = 'http://localhost'
11
+
12
+ /** Build the Web `Request` for an incoming message. The body, when the method has one, is streamed, not buffered. */
13
+ export function toRequest(req: IncomingMessage, signal: AbortSignal): Request {
14
+ let url: URL
15
+ try {
16
+ url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`)
17
+ } catch {
18
+ url = new URL(req.url ?? '/', FALLBACK_ORIGIN)
19
+ }
20
+ const headers = new Headers()
21
+ for (const [key, value] of Object.entries(req.headers)) {
22
+ if (value === undefined) continue
23
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item)
24
+ else headers.set(key, value)
25
+ }
26
+ const method = (req.method ?? 'GET').toUpperCase()
27
+ if (method === 'GET' || method === 'HEAD') return new Request(url, { method, headers, signal })
28
+ // Node's fetch requires `duplex: 'half'` for a streamed body; the DOM lib's `RequestInit` does not declare the field yet.
29
+ const init: RequestInit & { duplex: 'half' } = { method, headers, signal, body: Readable.toWeb(req) as ReadableStream, duplex: 'half' }
30
+ return new Request(url, init)
31
+ }
32
+
33
+ /**
34
+ * Write a Web `Response` to the node response: status, headers (repeated `set-cookie` kept separate) and the streamed body.
35
+ * A client that disconnects mid-stream ends the copy quietly; the caller's abort signal is what stops the handler.
36
+ */
37
+ export async function sendResponse(res: ServerResponse, response: Response, method: string): Promise<void> {
38
+ const headers: Record<string, string | string[]> = {}
39
+ response.headers.forEach((value, key) => { if (key !== 'set-cookie') headers[key] = value })
40
+ const cookies = response.headers.getSetCookie()
41
+ if (cookies.length > 0) headers['set-cookie'] = cookies
42
+ res.writeHead(response.status, headers)
43
+ // A stream may stay silent for a while; the status line and headers must reach the client now, not with the first chunk.
44
+ res.flushHeaders()
45
+ if (response.body === null || method === 'HEAD') {
46
+ res.end()
47
+ return
48
+ }
49
+ try {
50
+ await pipeline(Readable.fromWeb(response.body as import('node:stream/web').ReadableStream), res)
51
+ } catch {
52
+ if (!res.writableEnded) res.destroy()
53
+ }
54
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Web adapter for `acryl-router` (spec 046, milestone M2). A plugin that owns routes on the router asks for a path
3
+ * prefix to be served over HTTP: `ctx.routerHttp.mount('/api/market')` claims that prefix on the DSH `webServer` and every
4
+ * request under it is run through `router.fetch`. The TUI and Desktop do not need this: they call `router.fetch` directly.
5
+ */
6
+
7
+ import type { IncomingMessage, ServerResponse } from 'node:http'
8
+ import type { RouterHttpService } from '../../acryl-router/src/index.ts'
9
+ import { Context, Service } from '@deepseek-ai/cordis'
10
+ import { serve } from './serve.ts'
11
+
12
+ /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
13
+ export const name = 'acryl-router-http'
14
+
15
+ /** Hard requirements: both services must exist, otherwise this plugin stays PENDING. */
16
+ export const inject = ['router', 'webServer']
17
+
18
+ /**
19
+ * The slice of the DSH `webServer` this adapter uses, as its own port: a prefix claim that returns its remover. The service
20
+ * is read by name, so this package imports nothing from the DeepSeek Harness (the layout gate keeps that count falling).
21
+ */
22
+ export interface WebServerPort {
23
+ register(route: {
24
+ readonly kind: 'prefix'
25
+ readonly path: string
26
+ readonly handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>
27
+ }): () => void
28
+ }
29
+
30
+ const PREFIX_PATTERN = /^\/[A-Za-z0-9._~\-/]*[A-Za-z0-9._~-]$/
31
+
32
+ export class RouterHttp extends Service implements RouterHttpService {
33
+ private readonly router: Context['router']
34
+ private readonly web: WebServerPort
35
+
36
+ constructor(ctx: Context) {
37
+ super(ctx, 'routerHttp')
38
+ this.router = ctx.router
39
+ this.web = ctx.get('webServer') as WebServerPort
40
+ }
41
+
42
+ /**
43
+ * Serve `prefix` and everything below it through the router. Returns a remover; the mount also goes away when the
44
+ * calling plugin unloads. A prefix another route already claims throws, as the webServer treats it as misconfiguration.
45
+ */
46
+ mount(prefix: string): () => void {
47
+ if (!PREFIX_PATTERN.test(prefix)) throw new TypeError(`acryl-router-http: "${prefix}" is not a mountable path prefix`)
48
+ const remove = this.web.register({
49
+ kind: 'prefix',
50
+ path: prefix,
51
+ handler: (req, res) => serve(this.router, req, res),
52
+ })
53
+ this.ctx.effect(() => () => { remove() }, `acryl-router-http: mount ${prefix}`)
54
+ return remove
55
+ }
56
+ }
57
+
58
+ export function apply(ctx: Context): void {
59
+ new RouterHttp(ctx)
60
+ }
61
+
62
+ export { serve, type FetchRouter } from './serve.ts'
63
+ export { sendResponse, toRequest } from './bridge.ts'