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,188 @@
1
+ # Routing and mounting
2
+
3
+ Routing has two levels:
4
+
5
+ 1. Routes inside an `App`.
6
+ 2. Cordis mounts that place an `App` in the host at a base path.
7
+
8
+ Keeping those levels separate lets application code stay portable while the host controls the public route space.
9
+
10
+ ## HTTP methods
11
+
12
+ ```ts
13
+ app.get('/todos', listTodos)
14
+ app.post('/todos', createTodo)
15
+ app.put('/todos/:id', replaceTodo)
16
+ app.patch('/todos/:id', updateTodo)
17
+ app.delete('/todos/:id', deleteTodo)
18
+ app.options('/todos', options)
19
+ app.all('/health', health) // any method
20
+ app.on('PURGE', '/cache', purge) // a custom method
21
+ app.on(['PUT', 'DELETE'], '/post', handler)
22
+ app.on('GET', ['/hello', '/en/hello'], handler)
23
+ ```
24
+
25
+ `HEAD` is answered from the `GET` route with the body removed. A known path asked with another method answers 404 like an unknown path;
26
+ add the `methodNotAllowed` middleware to an app that should answer 405 with an `Allow` header instead.
27
+
28
+ ```ts
29
+ import { methodNotAllowed } from 'acryl-router/middleware'
30
+
31
+ const app = new App()
32
+ app.use(methodNotAllowed({ app })) // POST /item -> 405, Allow: GET, HEAD
33
+ app.get('/item', c => c.text('item'))
34
+ ```
35
+
36
+ Routes can be chained, and a chained call without a path reuses the last one:
37
+
38
+ ```ts
39
+ app
40
+ .get('/endpoint', c => c.text('GET /endpoint'))
41
+ .post(c => c.text('POST /endpoint'))
42
+ .delete(c => c.text('DELETE /endpoint'))
43
+ ```
44
+
45
+ Chaining also carries the route types, which the typed `testClient` and `acryl-router/client` read.
46
+
47
+ ## Path parameters
48
+
49
+ ```ts
50
+ app.get('/users/:userId/todos/:todoId', c => {
51
+ const { userId, todoId } = c.req.param()
52
+ return c.json({ userId, todoId })
53
+ })
54
+
55
+ app.get('/api/animal/:type?', c => c.text('Animal!')) // optional: /api/animal and /api/animal/cat
56
+ app.get('/post/:date{[0-9]+}/:title{[a-z]+}', handler) // constrained by a regular expression
57
+ app.get('/posts/:filename{.+\\.png}', handler) // a parameter that includes slashes
58
+ ```
59
+
60
+ Prefer explicit routes over complex patterns when both express the same rule.
61
+
62
+ ## Wildcards
63
+
64
+ ```ts
65
+ app.use('/admin/*', requireAdmin)
66
+ app.get('/wild/*/card', c => c.text('GET /wild/*/card'))
67
+ ```
68
+
69
+ A wildcard is appropriate for a prefix boundary. It should not hide distinct resources that callers need to address independently.
70
+
71
+ ## Priority
72
+
73
+ Handlers and middleware run in registration order, and the first handler that returns a response ends the request:
74
+
75
+ ```ts
76
+ app.get('/book/a', c => c.text('a')) // GET /book/a -> a
77
+ app.get('/book/:slug', c => c.text('common')) // GET /book/b -> common
78
+ ```
79
+
80
+ Put middleware above the handlers it should wrap and a fallback such as `app.get('*', ...)` below the others.
81
+
82
+ ## Grouping related routes
83
+
84
+ Build a smaller app for a feature, then place it inside another app:
85
+
86
+ ```ts
87
+ const todos = new App()
88
+ todos.get('/', listTodos)
89
+ todos.post('/', createTodo)
90
+ todos.get('/:id', showTodo)
91
+
92
+ const app = new App()
93
+ app.route('/todos', todos)
94
+ ```
95
+
96
+ `route` copies the routes the sub-app has at that moment. Register them first: a route added to `todos` after `app.route(...)` is not
97
+ part of `app`.
98
+
99
+ ## Base paths
100
+
101
+ ```ts
102
+ const api = new App().basePath('/api/v1')
103
+ api.get('/todos', listTodos) // GET /api/v1/todos
104
+ ```
105
+
106
+ Do not repeat a mount prefix both in `basePath` and in the outer mount unless the doubled path is deliberate.
107
+
108
+ ## Mounting from Cordis
109
+
110
+ A plugin mounts its app through the `router` service:
111
+
112
+ ```ts
113
+ export const name = 'todo'
114
+ export const inject = ['router']
115
+
116
+ export function apply(ctx: Context): void {
117
+ const app = createTodoApp()
118
+ ctx.router.route('/todo', app)
119
+ ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
120
+ }
121
+ ```
122
+
123
+ - The mount is owned by the calling plugin: `route` registers it as that plugin's effect, so unloading the plugin removes it and a reload
124
+ mounts again without duplicates. The returned function removes it sooner.
125
+ - `route` copies the app's routes at the call, like `app.route`; mount the app once it is complete.
126
+ - Mounts run in the order they were made, so middleware an earlier mount registers for a path (`app.use('/todo/*', ...)` mounted at `/`)
127
+ wraps routes a later mount serves below it. Two plugins may mount on the same path: the first one's matching route answers, and when it
128
+ unloads the second takes over.
129
+ - `ctx.router.route` is the only way to register with the router.
130
+ - To be reachable over HTTP inside the ACRYL Host, ask the adapter to serve the prefix with `routerHttp.mount(prefix)` (an optional
131
+ dependency, hence the inner `ctx.inject`). A standalone `acryl-node-server` serves the whole router, so there the call is not needed.
132
+
133
+ ## Not found and errors
134
+
135
+ An app's own error handler applies to that app's routes, also when the app is mounted:
136
+
137
+ ```ts
138
+ app.onError((error, c) => {
139
+ console.error(error)
140
+ return c.json({ error: 'internal_error' }, 500)
141
+ })
142
+ ```
143
+
144
+ A mounted app's `app.notFound` is not used: a request no mount claims is answered by the router. A plugin can replace that answer, and
145
+ the router's fallback error handler, once per router:
146
+
147
+ ```ts
148
+ ctx.router.notFound(c => c.json({ error: 'not_found' }, 404))
149
+ ctx.router.onError((error, c) => c.json({ error: 'internal_error' }, 500))
150
+ ```
151
+
152
+ Without them, an `HTTPException` becomes its own response, an aborted request is a 499 and anything else is logged and becomes a 500.
153
+ Return client errors from the handler that detected them (or throw an `HTTPException`); reserve `onError` for unexpected failures.
154
+
155
+ ## Trailing slashes
156
+
157
+ `/a` and `/a/` are different paths by default. That surprises people most on a mount's root: an app mounted at `/orders` with a route
158
+ `/` answers `/orders` and returns 404 on `/orders/`.
159
+
160
+ What each choice does for an app mounted at `/orders` with routes `/` and `/:id`, measured on the router:
161
+
162
+ | Choice | `/orders` | `/orders/` | `/orders/42` | `/orders/42/` |
163
+ | --- | --- | --- | --- | --- |
164
+ | nothing (the default) | 200 | 404 | 200 | 404 |
165
+ | `.use(trimTrailingSlash())` in the app | 200 | 301 to `/orders` | 200 | 301 to `/orders/42` |
166
+ | `acryl-router` row config `trailingSlash: 'ignore'` | 200 | 200 | 200 | 200 |
167
+ | `new App({ strict: false })` for the mounted app | 200 | 404 | 200 | 404 |
168
+
169
+ - **`trimTrailingSlash()`** (from `acryl-router/middleware`) is the choice a plugin makes for itself. It acts after the handlers: only
170
+ a GET or HEAD that would be a 404 and ends in `/` is redirected, so a path you did register with a trailing slash keeps working, and a
171
+ POST, PUT, PATCH or DELETE to `/orders/` stays a 404. `trimTrailingSlash({ alwaysRedirect: true })` redirects before the handlers run,
172
+ which a wildcard route (`/files/*`) needs. `appendTrailingSlash()` does the opposite, for an app whose canonical paths end in `/`.
173
+ - **`trailingSlash: 'ignore'`** is the router's own setting and applies to every mount at once, without a redirect. Set it in the
174
+ composition that owns the `acryl-router` row (the Blueprint or profile), not from one plugin.
175
+ - **`strict: false`** on an `App` is the core's option and works for an app you serve yourself (`app.fetch`, `app.request`). It has no
176
+ effect on a mounted app, because the router composes all mounts into one root app whose strictness comes from `trailingSlash`.
177
+
178
+ `examples/acryl-todo` uses `trimTrailingSlash()`: `/todo/` sends the browser to `/todo`, and the API keeps one path per route.
179
+
180
+ ## Testing routes
181
+
182
+ ```ts
183
+ const response = await app.request('/todos/42')
184
+ const body = await response.json()
185
+ ```
186
+
187
+ Test both the application in isolation and at least one mounted route through the Cordis router. The second test proves the mount,
188
+ prefix and disposal contract, not just handler logic.
@@ -0,0 +1,71 @@
1
+ # Surfaces and adapters
2
+
3
+ An ACRYL application is written once and reached through the surface that fits the user. Surfaces provide transport and presentation;
4
+ plugins provide capabilities.
5
+
6
+ ## Surface model
7
+
8
+ | Surface | How requests arrive | Typical use |
9
+ | --- | --- | --- |
10
+ | Web | HTTP to the ACRYL Host's web server, through `acryl-router-http` | browser applications and APIs |
11
+ | Desktop | the same Host web server on a loopback port, opened in the Electron window | local-first applications |
12
+ | Terminal UI | in process: `ctx.router.request(...)` or `ctx.router.fetch(...)` | operator tools, agents, terminal workflows |
13
+ | Standalone process | HTTP through `acryl-node-server` | an app that runs without the Host |
14
+ | Tests | `ctx.router.request(...)` or `app.request(...)` | handler and contract tests |
15
+
16
+ The handler does not know which row supplied the request. When it needs a transport fact it reads the connection from the context
17
+ (`connectionOf(c)`), which is empty for an in-process call.
18
+
19
+ ## Web
20
+
21
+ The Host owns the listening socket, the address and process shutdown. A plugin mounts its app on the router, then asks the adapter to
22
+ serve the prefix over HTTP:
23
+
24
+ ```ts
25
+ ctx.router.route('/todo', app)
26
+ ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
27
+ ```
28
+
29
+ Only prefixes asked for this way are served over HTTP. A prefix the Host's web server already serves cannot be claimed: `mount` throws,
30
+ so a plugin cannot take over a Host route by mounting on it.
31
+
32
+ ## Desktop
33
+
34
+ Desktop runs the same Host inside Electron, and its window talks to the Host's web server on a loopback port. A plugin's routes therefore
35
+ serve Desktop exactly as they serve Web, with no Desktop-specific code. Guard local control routes with `loopbackOnly` (see
36
+ [Middleware](./middleware.md#loopbackonly)).
37
+
38
+ A transport without a port, handing `ctx.router.fetch` to Electron's `protocol.handle`, needs no change to plugins and is a later task
39
+ (`specs/047-framework-port/plan.md` T014).
40
+
41
+ Desktop capabilities such as windows, files and native dialogs belong in declared Cordis services. A handler calls the service; it does not
42
+ import Electron.
43
+
44
+ ## Terminal UI
45
+
46
+ A terminal surface needs no server. It calls the composed router in process:
47
+
48
+ ```ts
49
+ const response = await ctx.router.request('/todo/todos')
50
+ const todos = await response.json()
51
+ ```
52
+
53
+ The `routerHttp` service does not exist there, so the plugin's `ctx.inject(['routerHttp'], ...)` simply never runs. Check the services a
54
+ host declares instead of assuming a network route exists.
55
+
56
+ ## What belongs where
57
+
58
+ | Concern | Owner |
59
+ | --- | --- |
60
+ | Route handlers and middleware | the feature plugin's `App` |
61
+ | Mount path and lifetime | the feature plugin, through `ctx.router.route` |
62
+ | Socket and port | the Host (`acryl-router-http`) or `acryl-node-server` |
63
+ | Terminal rendering and input | the terminal surface |
64
+ | Shared domain behavior | a domain service every surface uses |
65
+ | Shared reactive state | `acryl-state` or an application service |
66
+
67
+ ## Loopback-only routes
68
+
69
+ Some routes exist only for the local host, such as callbacks and control endpoints. Guard them with `loopbackOnly` at the framework
70
+ boundary; do not rely on an obscure path or a missing link as access control. Loopback restriction is separate from authentication: a
71
+ local route that does privileged work still needs the host's authorization policy.
@@ -0,0 +1,9 @@
1
+ {
2
+ "source": {
3
+ "repo": "acryldev/acryl",
4
+ "path": "docs/framework",
5
+ "commit": "64e7c9d2e787f74b2da82492644fbbb46a601074"
6
+ },
7
+ "files": 16,
8
+ "digest": "sha256:f41321a2c2da4a9348634c9a6a6d7a62632d7c5906fbcba30479d569ef4bc6ef"
9
+ }
@@ -0,0 +1,135 @@
1
+ # Validation
2
+
3
+ Validate untrusted input at the route boundary. `schemaValidator` takes any [Standard Schema](https://standardschema.dev): Schemastery
4
+ (the schema library Cordis already uses for plugin config), Zod and Valibot all implement it, so a route needs no extra dependency.
5
+
6
+ ## Validate JSON
7
+
8
+ ```ts
9
+ import z from '@deepseek-ai/schemastery'
10
+ import { App } from 'acryl-router'
11
+ import { schemaValidator } from 'acryl-router/helpers'
12
+
13
+ const NewTodo = z.object({
14
+ title: z.string().required(),
15
+ done: z.boolean().default(false),
16
+ })
17
+
18
+ const app = new App().post('/todos', schemaValidator('json', NewTodo), c => {
19
+ const todo = c.req.valid('json') // typed by the schema's output
20
+ return c.json({ todo }, 201)
21
+ })
22
+ ```
23
+
24
+ A validated route has three parts: a schema for one input target, the validator middleware for that target, and a handler that reads
25
+ `c.req.valid(target)` instead of the raw body.
26
+
27
+ ## Failures
28
+
29
+ When the schema reports issues the handler does not run and the answer is 400 with `Content-Type: application/json` and exactly this
30
+ shape:
31
+
32
+ ```ts
33
+ { error: `invalid ${target}`, issues: Array<{ message: string, path: string }> }
34
+ ```
35
+
36
+ ```json
37
+ { "error": "invalid json", "issues": [{ "message": "title must be 1 to 200 characters", "path": "title" }] }
38
+ ```
39
+
40
+ - `error` names the target: `invalid json`, `invalid query`, `invalid param`, ...
41
+ - There is one entry per issue the schema reported, in its order, with the schema's own `message`.
42
+ - `path` is the issue's path joined with dots: `["items", 0, "title"]` becomes `"items.0.title"`, and a path segment given as
43
+ `{ key }` (as Standard Schema allows) contributes its `key`.
44
+ - An issue without a path, which a hand-written Standard Schema often reports for a whole-value check, gets `"path": ""`.
45
+ `{ issues: [{ message: 'not allowed' }] }` answers `{ "error": "invalid json", "issues": [{ "message": "not allowed", "path": "" }] }`.
46
+ Give issues a path when a client should point at a field.
47
+
48
+ ### A JSON body without a JSON content type
49
+
50
+ A `json` target reads the body only when the request's `Content-Type` is JSON (`application/json`, with or without parameters such as
51
+ `; charset=utf-8`, or `application/*+json`). Any other content type, or none, is not an error by itself: the schema sees `{}`. So a
52
+ client that forgets the header gets a misleading field error for a field it did send:
53
+
54
+ ```sh
55
+ curl -X POST -d '{"title":"milk"}' http://127.0.0.1:<port>/todo/api # no content type: curl sends a form type
56
+ # 400 {"error":"invalid json","issues":[{"message":"title must be 1 to 200 characters","path":"title"}]}
57
+ ```
58
+
59
+ If you see a field error for a field the request clearly carries, check the request's `Content-Type` first. A route that wants to say so
60
+ explicitly can refuse other content types before the validator:
61
+
62
+ ```ts
63
+ import type { MiddlewareHandler } from 'acryl-router'
64
+
65
+ const jsonOnly: MiddlewareHandler = async (c, next) => {
66
+ // The same types the json target reads: application/json and application/*+json, parameters allowed.
67
+ const type = c.req.header('content-type')?.split(';', 1)[0]?.trim() ?? ''
68
+ if (!/^application\/([a-z.-]+\+)?json$/i.test(type)) return c.json({ error: 'content type must be JSON' }, 415)
69
+ await next()
70
+ }
71
+
72
+ app.post('/todos', jsonOnly, schemaValidator('json', NewTodo), createTodo)
73
+ ```
74
+
75
+ ### A body that is not JSON
76
+
77
+ A JSON content type with a body that does not parse fails before the schema runs: the answer is 400 with the text body
78
+ `Malformed JSON in request body` (a thrown `HTTPException`, so an `onError` handler can reshape it). The `form` target does the same for
79
+ a form body that does not parse, with the text `Malformed FormData request.`
80
+
81
+ Keep the public failure shape consistent across the application and do not leak internals in messages. An application that needs a
82
+ different body writes its own check with the core `validator` (below). Valid input is not proof that the caller may use it:
83
+ authentication and authorization stay separate checks.
84
+
85
+ ## Targets
86
+
87
+ | Target | Read from |
88
+ | --- | --- |
89
+ | `json` | the JSON body |
90
+ | `form` | a `multipart/form-data` or `application/x-www-form-urlencoded` body |
91
+ | `query` | the query string |
92
+ | `param` | route parameters |
93
+ | `header` | request headers (names in lowercase) |
94
+ | `cookie` | cookies |
95
+
96
+ Validate only the targets the route uses, and chain one validator per target:
97
+
98
+ ```ts
99
+ app.get('/todos/:id', schemaValidator('param', z.object({ id: z.natural() })), schemaValidator('query', Paging), handler)
100
+ ```
101
+
102
+ ## The core validator
103
+
104
+ `validator` (also from `acryl-router/helpers`) takes a function instead of a schema. Return the value the handler should see, or a
105
+ response to refuse:
106
+
107
+ ```ts
108
+ import { validator } from 'acryl-router/helpers'
109
+
110
+ app.post('/posts', validator('form', (value, c) => {
111
+ const body = value['body']
112
+ if (typeof body !== 'string') return c.text('Invalid!', 400)
113
+ return { body }
114
+ }), c => c.json({ body: c.req.valid('form').body }, 201))
115
+ ```
116
+
117
+ ## Limit the body first
118
+
119
+ Validation reads the whole body. Put `bodyLimit` in front of a validator on any route open to untrusted callers:
120
+
121
+ ```ts
122
+ import { bodyLimit } from 'acryl-router/middleware'
123
+
124
+ app.post('/todos', bodyLimit({ maxSize: 16 * 1024 }), schemaValidator('json', NewTodo), createTodo)
125
+ ```
126
+
127
+ ## Typed clients
128
+
129
+ The validated input types flow into the route types, so the typed `testClient` and `acryl-router/client` check the request a caller sends.
130
+ See [Helpers](./helpers.md#testing).
131
+
132
+ ## What not to validate twice
133
+
134
+ Validate shape at the HTTP boundary, then enforce domain rules in the domain service. The route checks that `title` is a string; the
135
+ todo service decides whether that title is allowed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acrylblends",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Scaffold Cordis apps: acrylblends init.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,9 @@
20
20
  "bin",
21
21
  "dist",
22
22
  "templates",
23
- "plugins"
23
+ "shared",
24
+ "plugins",
25
+ "docs"
24
26
  ],
25
27
  "scripts": {
26
28
  "build": "tsdown",
@@ -7,5 +7,6 @@
7
7
  "dependencies": {
8
8
  "@deepseek-ai/cordis": "4.0.4"
9
9
  },
10
- "mount": "root.plugin(inspect); ctx.inspect.plugins(), ctx.inspect.explain('name'), ctx.inspect.render()"
10
+ "mount": "root.plugin(inspect); ctx.inspect.plugins(), ctx.inspect.explain('name'), ctx.inspect.render()",
11
+ "omit": ["**/*.test.*", "**/*.case.ts"]
11
12
  }
@@ -36,10 +36,10 @@ export interface PluginReport {
36
36
  readonly requires: readonly string[]
37
37
  /** Services it provides. */
38
38
  readonly provides: readonly string[]
39
- /** Routes it registered on the router. */
39
+ /** Paths it mounted an app under on the router. */
40
+ readonly mounts: readonly string[]
41
+ /** What those mounts serve; middleware and `all` routes show as method `ALL`, the way the core keeps them. */
40
42
  readonly routes: readonly RouteReport[]
41
- /** Router middleware paths it registered. */
42
- readonly middleware: readonly string[]
43
43
  /** Every other labeled registration (event listeners, timers, commands, tools). */
44
44
  readonly other: readonly string[]
45
45
  /** How many registrations disappear when it unloads: all of them. */
@@ -47,13 +47,19 @@ export interface PluginReport {
47
47
  }
48
48
 
49
49
  const PROVIDE = /^ctx\.provide\("(.+)"\)$/
50
- const ROUTE = /^acryl-router: (GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|\*) (\/\S*)$/
51
- const USE = /^acryl-router: use (\/\S*)$/
50
+ const MOUNT = /^acryl-router: route (\S+)$/
51
+ const SERVES = /^acryl-router: serves ([A-Z]+) (\S+)$/
52
52
 
53
53
  function flatten(effects: readonly EffectLabel[]): string[] {
54
54
  return effects.flatMap(effect => [effect.label, ...flatten(effect.children)])
55
55
  }
56
56
 
57
+ /** A mount's children name the routes it serves; they describe that one registration and are not registrations of their own. */
58
+ function routeOf(label: string): RouteReport | undefined {
59
+ const served = SERVES.exec(label)
60
+ return served?.[1] !== undefined && served[2] !== undefined ? { method: served[1], path: served[2] } : undefined
61
+ }
62
+
57
63
  export function stateName(state: number): FiberStateName {
58
64
  return STATE_NAMES[state] ?? 'FAILED'
59
65
  }
@@ -74,17 +80,19 @@ export function ownerOf(fiber: FiberView): FiberView {
74
80
  /** Describe one fiber, counting the registrations of the unnamed inline callbacks it owns (`adopted`) as its own. */
75
81
  export function reportFiber(fiber: FiberView, adopted: readonly FiberView[] = []): PluginReport {
76
82
  const provides: string[] = []
83
+ const mounts: string[] = []
77
84
  const routes: RouteReport[] = []
78
- const middleware: string[] = []
79
85
  const other: string[] = []
80
86
  const labels = [fiber, ...adopted].flatMap(source => flatten(source.getEffects()))
87
+ let registrations = 0
81
88
  for (const label of labels) {
82
89
  const provided = PROVIDE.exec(label)
83
- const route = ROUTE.exec(label)
84
- const used = USE.exec(label)
90
+ const mount = MOUNT.exec(label)
91
+ const route = routeOf(label)
92
+ if (route !== undefined) { routes.push(route); continue }
93
+ registrations += 1
85
94
  if (provided?.[1] !== undefined) provides.push(provided[1])
86
- else if (route?.[1] !== undefined && route[2] !== undefined) routes.push({ method: route[1], path: route[2] })
87
- else if (used?.[1] !== undefined) middleware.push(used[1])
95
+ else if (mount?.[1] !== undefined) mounts.push(mount[1])
88
96
  else other.push(label)
89
97
  }
90
98
  return {
@@ -92,10 +100,10 @@ export function reportFiber(fiber: FiberView, adopted: readonly FiberView[] = []
92
100
  state: stateName(fiber.state),
93
101
  requires: Object.keys(fiber.inject).sort(),
94
102
  provides: provides.sort(),
103
+ mounts,
95
104
  routes,
96
- middleware,
97
105
  other,
98
- removedOnUnload: labels.length,
106
+ removedOnUnload: registrations,
99
107
  }
100
108
  }
101
109
 
@@ -108,7 +116,7 @@ export function explain(report: PluginReport): string {
108
116
  ` needs: ${list(report.requires)}`,
109
117
  ` provides: ${list(report.provides)}`,
110
118
  ` routes: ${list(report.routes.map(route => `${route.method} ${route.path}`))}`,
111
- ` middleware:${report.middleware.length === 0 ? ' nothing' : ` ${report.middleware.join(', ')}`}`,
119
+ ` mounts: ${report.mounts.length === 0 ? 'nothing' : report.mounts.join(', ')}`,
112
120
  ]
113
121
  if (report.other.length > 0) lines.push(` also: ${report.other.join('; ')}`)
114
122
  lines.push(report.state === 'PENDING'
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { Context, Service } from '@deepseek-ai/cordis'
8
8
  import { loopbackOnly } from '../../acryl-router/src/middleware/index.ts'
9
- import type {} from '../../acryl-router/src/index.ts'
9
+ import { App } from '../../acryl-router/src/index.ts'
10
10
  import { explain, ownerOf, renderReports, reportFiber, type FiberView, type PluginReport } from './report.ts'
11
11
 
12
12
  declare module '@deepseek-ai/cordis' {
@@ -54,12 +54,14 @@ export function apply(ctx: Context): void {
54
54
  // The same facts on the router, for any surface; present only where a router is. A request over a socket must come from this machine to this app;
55
55
  // the terminal calls the router in process and is the host's own code.
56
56
  ctx.inject(['router'], inner => {
57
- const api = inner.router.group('/api/inspect', loopbackOnly({ inProcess: 'allow' }))
58
- api.get('/plugins', c => c.json(inspect.plugins()))
59
- api.get('/plugins/:name', c => {
60
- const text = inspect.explain(c.param('name') ?? '')
61
- return text === undefined ? c.json({ error: 'no such plugin' }, 404) : c.text(text)
62
- })
57
+ const api = new App()
58
+ .use(loopbackOnly({ inProcess: 'allow' }))
59
+ .get('/plugins', c => c.json(inspect.plugins()))
60
+ .get('/plugins/:name', c => {
61
+ const text = inspect.explain(c.req.param('name'))
62
+ return text === undefined ? c.json({ error: 'no such plugin' }, 404) : c.text(text)
63
+ })
64
+ inner.router.route('/api/inspect', api)
63
65
  // Over HTTP only where an adapter exists (Web and Desktop); the terminal needs none.
64
66
  inner.inject(['routerHttp'], http => { http.routerHttp.mount('/api/inspect') })
65
67
  })
@@ -7,14 +7,19 @@ function fiber(over: Partial<FiberView> & { effects?: FiberView['getEffects'] ex
7
7
  }
8
8
 
9
9
  describe('reportFiber', () => {
10
- it('sorts effects into provides, routes, middleware and everything else, including nested ones', () => {
10
+ it('sorts effects into provides, mounts with their routes, and everything else, including nested ones', () => {
11
11
  const report = reportFiber(fiber({
12
12
  inject: { router: null, acrylSettings: null },
13
13
  effects: [
14
14
  { label: 'ctx.provide("things")', children: [] },
15
- { label: 'acryl-router: GET /api/things/:id', children: [] },
16
- { label: 'acryl-router: POST /api/things', children: [{ label: 'inner timer', children: [] }] },
17
- { label: 'acryl-router: use /api', children: [] },
15
+ { label: 'acryl-router: route /api', children: [
16
+ { label: 'acryl-router: serves ALL /api/*', children: [] },
17
+ { label: 'acryl-router: serves GET /api/things/:id', children: [] },
18
+ ] },
19
+ { label: 'acryl-router: route /api/things', children: [
20
+ { label: 'acryl-router: serves POST /api/things', children: [] },
21
+ { label: 'inner timer', children: [] },
22
+ ] },
18
23
  { label: 'ctx.on("internal/status")', children: [] },
19
24
  ],
20
25
  }))
@@ -23,10 +28,10 @@ describe('reportFiber', () => {
23
28
  state: 'ACTIVE',
24
29
  requires: ['acrylSettings', 'router'],
25
30
  provides: ['things'],
26
- routes: [{ method: 'GET', path: '/api/things/:id' }, { method: 'POST', path: '/api/things' }],
27
- middleware: ['/api'],
31
+ mounts: ['/api', '/api/things'],
32
+ routes: [{ method: 'ALL', path: '/api/*' }, { method: 'GET', path: '/api/things/:id' }, { method: 'POST', path: '/api/things' }],
28
33
  other: ['inner timer', 'ctx.on("internal/status")'],
29
- removedOnUnload: 6,
34
+ removedOnUnload: 5,
30
35
  })
31
36
  })
32
37
 
@@ -42,7 +47,7 @@ describe('explain', () => {
42
47
  it('says what a plugin needs, adds and removes on unload', () => {
43
48
  const text = explain(reportFiber(fiber({
44
49
  inject: { router: null },
45
- effects: [{ label: 'acryl-router: GET /a', children: [] }],
50
+ effects: [{ label: 'acryl-router: route /', children: [{ label: 'acryl-router: serves GET /a', children: [] }] }],
46
51
  })))
47
52
  expect(text).toContain('demo (ACTIVE)')
48
53
  expect(text).toContain('needs: router')
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Context } from '@deepseek-ai/cordis'
7
- import { apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
7
+ import { App, apply as routerApply, name as routerName } from '../../acryl-router/src/index.ts'
8
8
  import { afterEach, describe, expect, it } from 'vitest'
9
9
  import { apply, name, type PluginReport } from '../src/index.ts'
10
10
 
@@ -18,9 +18,10 @@ const market = {
18
18
  name: 'demo-market',
19
19
  inject: ['router'],
20
20
  apply(ctx: Context): void {
21
- ctx.router.use('/api/demo', async (_c, next) => next())
22
- ctx.router.get('/api/demo/state', c => c.json({ ok: true }))
23
- ctx.router.post('/api/demo/sources', c => c.json({ ok: true }, 201))
21
+ ctx.router.route('/api/demo', new App()
22
+ .use(async (_c, next) => { await next() })
23
+ .get('/state', c => c.json({ ok: true }))
24
+ .post('/sources', c => c.json({ ok: true }, 201)))
24
25
  },
25
26
  }
26
27
 
@@ -45,7 +46,7 @@ describe('acryl-inspect', () => {
45
46
  ctx.plugin({
46
47
  name: 'demo-inline',
47
48
  apply(inner: Context): void {
48
- inner.inject(['router'], deeper => { deeper.router.get('/api/inline', c => c.json({ ok: true })) })
49
+ inner.inject(['router'], deeper => { deeper.router.route('/', new App().get('/api/inline', c => c.json({ ok: true }))) })
49
50
  },
50
51
  })
51
52
  await settle()
@@ -67,9 +68,9 @@ describe('acryl-inspect', () => {
67
68
  state: 'ACTIVE',
68
69
  requires: ['router'],
69
70
  provides: [],
70
- routes: [{ method: 'GET', path: '/api/demo/state' }, { method: 'POST', path: '/api/demo/sources' }],
71
- middleware: ['/api/demo'],
72
- removedOnUnload: 3,
71
+ mounts: ['/api/demo'],
72
+ routes: [{ method: 'ALL', path: '/api/demo/*' }, { method: 'GET', path: '/api/demo/state' }, { method: 'POST', path: '/api/demo/sources' }],
73
+ removedOnUnload: 1,
73
74
  })
74
75
  })
75
76
 
@@ -102,10 +103,10 @@ describe('acryl-inspect', () => {
102
103
  const list = await ctx.router.fetch(new Request('http://acryl.test/api/inspect/plugins'))
103
104
  expect(list.status).toBe(200)
104
105
  const reports = await list.json() as PluginReport[]
105
- expect(byName(reports, 'demo-market').routes).toHaveLength(2)
106
+ expect(byName(reports, 'demo-market').routes).toHaveLength(3)
106
107
 
107
108
  const one = await ctx.router.fetch(new Request('http://acryl.test/api/inspect/plugins/demo-market'))
108
- expect(await one.text()).toContain('removes 3 registrations and nothing else')
109
+ expect(await one.text()).toContain('removes 1 registration and nothing else')
109
110
  expect((await ctx.router.fetch(new Request('http://acryl.test/api/inspect/plugins/nope'))).status).toBe(404)
110
111
  })
111
112