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.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +84 -5
- package/dist/index.js.map +1 -1
- package/docs/framework/LICENSE +23 -0
- package/docs/framework/README.md +63 -0
- package/docs/framework/UPSTREAM.md +3 -0
- package/docs/framework/api-summary.md +138 -0
- package/docs/framework/concepts.md +105 -0
- package/docs/framework/context.md +129 -0
- package/docs/framework/getting-started.md +146 -0
- package/docs/framework/helpers.md +118 -0
- package/docs/framework/middleware.md +154 -0
- package/docs/framework/migration-from-0-3.md +108 -0
- package/docs/framework/migration-from-dsh-webserver.md +103 -0
- package/docs/framework/node-server.md +75 -0
- package/docs/framework/plugin-lifecycle.md +111 -0
- package/docs/framework/request.md +106 -0
- package/docs/framework/routing.md +188 -0
- package/docs/framework/surfaces.md +71 -0
- package/docs/framework/upstream.json +9 -0
- package/docs/framework/validation.md +135 -0
- package/package.json +4 -2
- package/plugins/acryl-inspect/plugin.json +2 -1
- package/plugins/acryl-inspect/src/report.ts +21 -13
- package/plugins/acryl-inspect/src/service.ts +9 -7
- package/plugins/acryl-inspect/tests/report.spec.ts +13 -8
- package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
- package/plugins/acryl-inspect/upstream.json +2 -2
- package/plugins/acryl-node-server/LICENSE +26 -0
- package/plugins/acryl-node-server/plugin.json +2 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +19 -6
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
- package/plugins/acryl-node-server/upstream.json +3 -3
- package/plugins/acryl-router/LICENSE +26 -0
- package/plugins/acryl-router/plugin.json +2 -1
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -3
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -6
- package/plugins/acryl-router/src/middleware/index.ts +25 -6
- package/plugins/acryl-router/src/middleware/loopback-only.ts +53 -14
- package/plugins/acryl-router/src/service.ts +262 -64
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
- package/plugins/acryl-router/tests/loader.spec.ts +3 -2
- package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
- package/plugins/acryl-router/tests/service.spec.ts +281 -66
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/plugin.json +2 -1
- package/plugins/acryl-router-http/src/bridge.ts +3 -1
- package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
- package/plugins/acryl-router-http/upstream.json +3 -3
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +1 -1
- package/plugins/acryl-state/src/cell.ts +1 -1
- package/plugins/acryl-state/src/service.ts +33 -15
- package/plugins/acryl-state/upstream.json +6 -4
- package/plugins/builder/script.ts +4 -7
- package/plugins/extensions/index.ts +6 -5
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +9 -7
- package/plugins/tsconfig.json +5 -4
- package/shared/AGENTS.core.md +54 -0
- package/templates/agent/AGENTS.md +6 -0
- package/templates/agent/README.md +2 -2
- package/templates/agent/blend.yaml +1 -1
- package/templates/agent/src/routes.ts +37 -27
- package/templates/agent/test/model.test.ts +2 -1
- package/templates/agent/tsconfig.json +8 -5
- package/templates/blank/AGENTS.md +5 -0
- package/templates/blank/README.md +1 -1
- package/templates/blank/src/routes.ts +6 -4
- package/templates/blank/tsconfig.json +2 -4
- package/templates/todo/AGENTS.md +5 -0
- package/templates/todo/src/page.ts +1 -1
- package/templates/todo/src/todo.ts +58 -29
- package/templates/todo/tsconfig.json +2 -4
- package/templates/todo/upstream.json +2 -2
- package/plugins/acryl-router/src/compose.ts +0 -21
- package/plugins/acryl-router/src/context.ts +0 -59
- package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
- package/plugins/acryl-router/src/helpers/sse.ts +0 -83
- package/plugins/acryl-router/src/helpers/validate.ts +0 -44
- package/plugins/acryl-router/src/http-error.ts +0 -21
- package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
- package/plugins/acryl-router/src/middleware/cors.ts +0 -46
- package/plugins/acryl-router/src/middleware/logger.ts +0 -21
- package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
- package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
- package/plugins/acryl-router/src/pattern.ts +0 -89
- package/plugins/acryl-router/src/registrar.ts +0 -90
- package/plugins/acryl-router/src/table.ts +0 -156
- package/plugins/acryl-router/src/types.ts +0 -73
- package/plugins/acryl-router/src/util.ts +0 -23
- package/plugins/acryl-router/tests/api.spec.ts +0 -223
- package/plugins/acryl-router/tests/conformance.ts +0 -170
- package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
- package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
- 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,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
|
+
"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
|
-
"
|
|
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
|
-
/**
|
|
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
|
|
51
|
-
const
|
|
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
|
|
84
|
-
const
|
|
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 (
|
|
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:
|
|
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
|
-
`
|
|
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
|
|
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 =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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,
|
|
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:
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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:
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
removedOnUnload:
|
|
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(
|
|
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
|
|
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
|
|