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,108 @@
|
|
|
1
|
+
# Migration from 0.3
|
|
2
|
+
|
|
3
|
+
0.3.0 had the framework's own small router API: routes registered one at a time on `ctx.router`, and a `RouteContext` with a few
|
|
4
|
+
helpers. 0.4.0 replaces it with the ported application API: a plugin builds an `App` and mounts it. The old calls are removed, not kept
|
|
5
|
+
as aliases, so there is one way to write a route.
|
|
6
|
+
|
|
7
|
+
## Before and after
|
|
8
|
+
|
|
9
|
+
0.3.0:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { HttpError } from 'acryl-router'
|
|
13
|
+
import { readJson, streamSSE, valid, validate } from 'acryl-router/helpers'
|
|
14
|
+
import { bodyLimit } from 'acryl-router/middleware'
|
|
15
|
+
|
|
16
|
+
export function apply(ctx: Context): void {
|
|
17
|
+
const api = ctx.router.group('/todo')
|
|
18
|
+
api.use(bodyLimit({ maxBytes: 16 * 1024 }))
|
|
19
|
+
api.get('/todos/:id', c => {
|
|
20
|
+
const todo = find(c.param('id'))
|
|
21
|
+
if (!todo) throw new HttpError(404, { message: 'no such todo' })
|
|
22
|
+
return c.json(todo)
|
|
23
|
+
})
|
|
24
|
+
api.post('/todos', validate('json', NewTodo), c => c.json(create(valid(c, 'json')), 201))
|
|
25
|
+
api.get('/events', c => streamSSE(c, stream => {
|
|
26
|
+
const off = events.on('change', todo => stream.send(todo, { event: 'change' }))
|
|
27
|
+
return off
|
|
28
|
+
}))
|
|
29
|
+
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
0.4.0:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { App, HTTPException } from 'acryl-router'
|
|
37
|
+
import { schemaValidator, streamSSE } from 'acryl-router/helpers'
|
|
38
|
+
import { bodyLimit } from 'acryl-router/middleware'
|
|
39
|
+
|
|
40
|
+
export function apply(ctx: Context): void {
|
|
41
|
+
const app = new App()
|
|
42
|
+
.use(bodyLimit({ maxSize: 16 * 1024 }))
|
|
43
|
+
.get('/todos/:id', c => {
|
|
44
|
+
const todo = find(c.req.param('id'))
|
|
45
|
+
if (!todo) throw new HTTPException(404, { message: 'no such todo' })
|
|
46
|
+
return c.json(todo)
|
|
47
|
+
})
|
|
48
|
+
.post('/todos', schemaValidator('json', NewTodo), c => c.json(create(c.req.valid('json')), 201))
|
|
49
|
+
.get('/events', c => streamSSE(c, async stream => {
|
|
50
|
+
const off = events.on('change', todo => { void stream.writeSSE({ event: 'change', data: JSON.stringify(todo) }) })
|
|
51
|
+
stream.onAbort(off)
|
|
52
|
+
while (!stream.aborted) await stream.sleep(15_000).then(() => stream.write(': keep-alive\n\n'))
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
ctx.router.route('/todo', app)
|
|
56
|
+
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Public paths do not change: the mount path plus the app's route path is the old group prefix plus the old route path.
|
|
61
|
+
|
|
62
|
+
## What maps to what
|
|
63
|
+
|
|
64
|
+
| 0.3.0 | 0.4.0 |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `ctx.router.get/post/put/patch/delete/all/on(path, ...)` | the same methods on an `App`, then `ctx.router.route(prefix, app)` |
|
|
67
|
+
| `ctx.router.use(...)` | `app.use(...)` on the app it should wrap (or on an app mounted earlier, for a guard in front of other plugins) |
|
|
68
|
+
| `ctx.router.group(prefix, ...middleware)` | one `App` per group mounted at `prefix`, or `app.route(prefix, sub)` / `app.basePath(prefix)` |
|
|
69
|
+
| the remover each registration returned | the one remover `ctx.router.route` returns; the mount is owned by the calling plugin as before |
|
|
70
|
+
| `ctx.router.onError`, `ctx.router.notFound` | unchanged; an app's own `app.onError` now also applies to its routes |
|
|
71
|
+
| `c.param(name)`, `c.params` | `c.req.param(name)`, `c.req.param()` |
|
|
72
|
+
| `c.query(name)`, `c.query()` | `c.req.query(name)`, `c.req.query()` (and `c.req.queries(name)` for repeated values) |
|
|
73
|
+
| `c.url` | `new URL(c.req.url)`, or `c.req.path` |
|
|
74
|
+
| `c.req` (the Web `Request`) | `c.req.raw`; `c.req` is now the request wrapper |
|
|
75
|
+
| `c.connection` | `connectionOf(c)` (or `getConnInfo(c)`); type the app `new App<RouterEnv>()` to read `c.env.connection` |
|
|
76
|
+
| `c.signal` | `c.req.raw.signal` |
|
|
77
|
+
| `c.waitUntil(task)` | `c.executionCtx.waitUntil(task)` |
|
|
78
|
+
| `c.get(key)` / `c.set(key, value)` | unchanged, typed by the app's `Variables` (`c.var.key` also works) |
|
|
79
|
+
| `c.json/text/html/body/redirect/notFound/header/status` | unchanged |
|
|
80
|
+
| `HttpError(status, { message, response, cause })` | `HTTPException(status, { message, res, cause })` |
|
|
81
|
+
| `readJson(c)`, `readText(c)`, `readBytes(c)` with `{ maxBytes }` | `bodyLimit({ maxSize })` in front, then `c.req.json()`, `c.req.text()`, `c.req.arrayBuffer()` |
|
|
82
|
+
| `bodyLimit({ maxBytes })` | `bodyLimit({ maxSize })` |
|
|
83
|
+
| `validate(target, schema)` and `valid(c, target)` | `schemaValidator(target, schema)` and `c.req.valid(target)`, with more targets (`form`, `header`, `cookie`) |
|
|
84
|
+
| `streamSSE(c, stream => cleanup)` with `stream.send(data, { event, id })` | `streamSSE(c, async stream => ...)` with `stream.writeSSE({ data, event, id })`; `data` is a string, so `JSON.stringify` objects |
|
|
85
|
+
| the SSE stream's automatic keep-alive comment | none: write a comment on a timer when a proxy closes idle connections |
|
|
86
|
+
| the cleanup function `run` returned | `stream.onAbort(cleanup)`; the callback's end closes the stream |
|
|
87
|
+
| a 405 with `Allow` for a known path and another method | 404 by default; add the `methodNotAllowed` middleware for 405 |
|
|
88
|
+
| the `Middleware` type, `(c, next) => Response` | `MiddlewareHandler`, `async (c, next) => { await next() }`; change `c.res` after `next()` instead of returning it |
|
|
89
|
+
| `logger`, `cors`, `requestId`, `secureHeaders`, `loopbackOnly` | same names in `acryl-router/middleware`; the first four now take the core's options |
|
|
90
|
+
|
|
91
|
+
## Behavior changes to check
|
|
92
|
+
|
|
93
|
+
- **A mount is a snapshot.** Routes an app registers after `ctx.router.route(...)` are not served. 0.3.0 registered each call live.
|
|
94
|
+
- **A JSON validator needs a JSON `Content-Type`.** Without it the schema sees `{}`. 0.3.0 read the body whatever its type.
|
|
95
|
+
- **`await next()` does not throw.** A thrown error has already become a response; read it from `c.error`.
|
|
96
|
+
- **Trailing slashes are strict by default, as before**, and `trailingSlash: 'ignore'` still treats `/a` and `/a/` as one path.
|
|
97
|
+
- **Disposal is unchanged:** a plugin's unload ends the requests and streams its code is serving, and router disposal ends all of them,
|
|
98
|
+
bounded by `disposeGraceMs`.
|
|
99
|
+
|
|
100
|
+
## Checklist
|
|
101
|
+
|
|
102
|
+
For each plugin:
|
|
103
|
+
|
|
104
|
+
- collect its route registrations into one app per mount path and keep the public paths;
|
|
105
|
+
- move middleware and error handling onto the owning app;
|
|
106
|
+
- replace the context and helper calls from the table above;
|
|
107
|
+
- run its route tests, then one activation test that unloads and reloads the plugin;
|
|
108
|
+
- leave no 0.3.0-style calls behind: the old API is gone, and two styles would give new code a second API to copy.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Migration from the DSH webServer
|
|
2
|
+
|
|
3
|
+
ACRYL plugins have registered HTTP routes directly with the DSH `webServer` (or the Host's `acrylWeb` wrapper of it): one Node-style handler
|
|
4
|
+
per exact path or prefix, with no middleware chain. The framework replaces that with an `App`, a mount, and Web-standard requests.
|
|
5
|
+
|
|
6
|
+
## Before and after
|
|
7
|
+
|
|
8
|
+
Before, a plugin claims a path and writes the Node response itself:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export function apply(ctx: Context): void {
|
|
12
|
+
ctx.effect(() => ctx.acrylWeb.register({
|
|
13
|
+
kind: 'exact',
|
|
14
|
+
path: '/api/health',
|
|
15
|
+
handler: (req, res) => {
|
|
16
|
+
res.statusCode = 200
|
|
17
|
+
res.setHeader('content-type', 'application/json')
|
|
18
|
+
res.end(JSON.stringify({ ok: true }))
|
|
19
|
+
},
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After, it builds an app, mounts it, and asks the Host adapter to serve the prefix:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { App } from 'acryl-router'
|
|
28
|
+
|
|
29
|
+
export const inject = ['router']
|
|
30
|
+
|
|
31
|
+
export function apply(ctx: Context): void {
|
|
32
|
+
const app = new App().get('/health', c => c.json({ ok: true }))
|
|
33
|
+
ctx.router.route('/api', app)
|
|
34
|
+
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/api/health') })
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`routerHttp.mount(prefix)` claims the prefix on the same web server, so the public path does not change. The terminal UI and tests reach
|
|
39
|
+
the route in process (`ctx.router.request('/api/health')`), which the old handler never allowed.
|
|
40
|
+
|
|
41
|
+
## What maps to what
|
|
42
|
+
|
|
43
|
+
| webServer | Framework |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `register({ kind: 'exact', path, handler })` | a route on an `App`, mounted with `ctx.router.route`; served with `routerHttp.mount(path)` |
|
|
46
|
+
| `register({ kind: 'prefix', path, handler })` | an `App` mounted at `path` with its own routes below it |
|
|
47
|
+
| `req.method` checks in the handler | `app.get`, `app.post`, ... (add `methodNotAllowed` for 405 answers) |
|
|
48
|
+
| `new URL(req.url, ...)` and manual parsing | `c.req.param`, `c.req.query`, `c.req.header` |
|
|
49
|
+
| reading the body stream by hand | `c.req.json()`, `c.req.parseBody()` behind `bodyLimit`, or a validator |
|
|
50
|
+
| `res.statusCode`, `res.setHeader`, `res.end` | `c.json`, `c.text`, `c.body`, `c.header`, or a returned `Response` |
|
|
51
|
+
| `req.socket.remoteAddress` | `connectionOf(c)` / `getConnInfo(c)` |
|
|
52
|
+
| a peer or origin check copied into each handler | `loopbackOnly`, `cors`, `csrf` or a middleware of your own |
|
|
53
|
+
| the disposer returned by `register` | nothing: `route` and `mount` belong to the calling plugin |
|
|
54
|
+
|
|
55
|
+
## Migration steps
|
|
56
|
+
|
|
57
|
+
Move one capability at a time:
|
|
58
|
+
|
|
59
|
+
1. Build an `App` for the capability and move its handlers into it without changing their public answers.
|
|
60
|
+
2. Mount it with `ctx.router.route` and serve the same prefix with `routerHttp.mount`.
|
|
61
|
+
3. Remove the direct `webServer` registration in the same change: the two cannot claim one prefix, `mount` throws.
|
|
62
|
+
4. Test the route through the router (`ctx.router.request`) and over the Host, and unload and reload the plugin to confirm the route goes
|
|
63
|
+
away and comes back once.
|
|
64
|
+
|
|
65
|
+
Keep domain calls unchanged until the transport change is green; do not rewrite transport, handlers and authorization in one step.
|
|
66
|
+
|
|
67
|
+
## A worked example: the workspace
|
|
68
|
+
|
|
69
|
+
`acryl-workspace` (files, git, terminals, projects, agents) was the largest user of the webServer: 24 exact routes. It now builds one app
|
|
70
|
+
per domain folder (`filesRoutes`, `gitRoutes`, `ptyRoutes`, ...), composes them once (`privateApi` in `src/private-route.ts`), mounts the
|
|
71
|
+
result after the effect that builds its services, and serves the one prefix `/api/acryl-workspace`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const built: { api?: App } = {}
|
|
75
|
+
ctx.effect(() => {
|
|
76
|
+
// ... the PTY table, git, files, the project registry ...
|
|
77
|
+
built.api = privateApi(filesRoutes(files, report), gitRoutes(git, report) /* , ... */)
|
|
78
|
+
return async () => { /* release them */ }
|
|
79
|
+
})
|
|
80
|
+
ctx.router.route('/', built.api)
|
|
81
|
+
ctx.routerHttp.mount('/api/acryl-workspace')
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Mounting after the effect orders the unload: the mount goes first, so the requests and event streams its code is serving end and are
|
|
85
|
+
waited for, then the services are released. Each route names its guard (`loopbackOnly({ callers: 'own-page' })` for the page, a bearer token
|
|
86
|
+
for the agent hooks) and its body limit; the event stream is `streamSSE`, and its keep-alive is one timer the plugin owns, not one per
|
|
87
|
+
request. The terminal's WebSocket stays a webServer upgrade until the router carries upgrades (spec 047 T009).
|
|
88
|
+
|
|
89
|
+
The answers that changed are recorded with it: a body over a route's limit is 413 on every route (some said 400); a wrong method is 405 with
|
|
90
|
+
an `Allow` header on every route; a missing or wrong agent-hook token is 401 with `WWW-Authenticate` (it was 403); and the hook's `Host`
|
|
91
|
+
must be the app's own loopback address. Its routes refuse an in-process call on purpose (the guard needs a socket to check), so its tests
|
|
92
|
+
run over loopback through the same adapter (`tests/serve-api.ts`), which is the "test over the Host" below.
|
|
93
|
+
|
|
94
|
+
## Completion check
|
|
95
|
+
|
|
96
|
+
A capability is migrated when:
|
|
97
|
+
|
|
98
|
+
- no handler in it touches Node `req` / `res` objects or the old server types;
|
|
99
|
+
- its public paths and answers are unchanged, or the changes are recorded;
|
|
100
|
+
- it unloads and reloads without duplicate or missing routes;
|
|
101
|
+
- the same handler passes an in-process test and a test over the Host.
|
|
102
|
+
|
|
103
|
+
Typed RPC stays a separate concern. Do not convert RPC methods because their names look like HTTP routes.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Node server
|
|
2
|
+
|
|
3
|
+
`acryl-node-server` serves the whole router over HTTP from a standalone Node.js process. Use it for an app that runs without the ACRYL
|
|
4
|
+
Host. Inside the Host (Web and Desktop), the Host owns the web server and a plugin asks `acryl-router-http` to serve its prefix instead.
|
|
5
|
+
|
|
6
|
+
## Serve the router
|
|
7
|
+
|
|
8
|
+
The server is a Cordis plugin that provides the `nodeServer` service:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import * as router from 'acryl-router'
|
|
13
|
+
import * as nodeServer from 'acryl-node-server'
|
|
14
|
+
import * as todo from './todo.ts'
|
|
15
|
+
|
|
16
|
+
const root = new Context()
|
|
17
|
+
root.plugin(router)
|
|
18
|
+
root.plugin(nodeServer, { port: 3000 })
|
|
19
|
+
root.plugin(todo)
|
|
20
|
+
|
|
21
|
+
root.inject(['nodeServer'], ctx => {
|
|
22
|
+
console.log(`listening on ${ctx.nodeServer.url}`)
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Option | Default | Meaning |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `hostname` | `127.0.0.1` | address to bind; binding elsewhere exposes the app to the network |
|
|
29
|
+
| `port` | `0` | port to listen on; 0 lets the system choose |
|
|
30
|
+
| `scan` | `20` | how many further ports to try when the configured one is taken |
|
|
31
|
+
|
|
32
|
+
The plugin becomes active once the server listens, so a plugin that injects `nodeServer` can rely on `ctx.nodeServer.url` and
|
|
33
|
+
`ctx.nodeServer.port`. A failure to bind fails the plugin.
|
|
34
|
+
|
|
35
|
+
Every request goes to `ctx.router.fetch`, so routes appear and disappear as plugins mount and unload, with no restart. A plugin's
|
|
36
|
+
`ctx.inject(['routerHttp'], ...)` call never fires here: this adapter serves every prefix already.
|
|
37
|
+
|
|
38
|
+
## Connection information
|
|
39
|
+
|
|
40
|
+
The adapter passes the socket's peer address and local port to the router as connection facts. Handlers read them through
|
|
41
|
+
`connectionOf(c)` or `getConnInfo(c)` and never touch Node socket objects, so the same handler runs in process and through the Host. See
|
|
42
|
+
[Context](./context.md#transport-facts).
|
|
43
|
+
|
|
44
|
+
## Static files
|
|
45
|
+
|
|
46
|
+
The Node `serveStatic` reads files from disk:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { serveStatic } from 'acryl-node-server'
|
|
50
|
+
|
|
51
|
+
const app = new App()
|
|
52
|
+
.use('/assets/*', serveStatic({ root: './public' }))
|
|
53
|
+
.get('/', serveStatic({ path: './public/index.html' }))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`root` is joined with the request path, relative to the working directory unless absolute. A request path with a `.` or `..` segment, a
|
|
57
|
+
doubled slash, a backslash or (unless `allowPercentInPath`) an encoded `%` is treated as not found and passed on with `next()`. Only `GET`
|
|
58
|
+
and `HEAD` are served. Other options: `rewriteRequestPath`, `precompressed` (serve a `.br`, `.zst` or `.gz` sibling when the client
|
|
59
|
+
accepts it), `index` (default `index.html`), and `onFound` / `onNotFound` hooks. Serve build output, never source folders.
|
|
60
|
+
|
|
61
|
+
## Shutdown
|
|
62
|
+
|
|
63
|
+
Disposing the plugin (or the root context) ends open streams, closes every connection and returns once the port is free. Disposing a
|
|
64
|
+
plugin that only mounted routes is different: its mount and the requests it is serving end, and the server keeps serving the others (see
|
|
65
|
+
[Plugin lifecycle](./plugin-lifecycle.md#disposal-sequence)).
|
|
66
|
+
|
|
67
|
+
## Choosing a port
|
|
68
|
+
|
|
69
|
+
Let the process configuration choose the address. Do not hard-code a development port in a reusable plugin: a plugin mounts routes and
|
|
70
|
+
leaves the socket to whichever adapter the app runs with.
|
|
71
|
+
|
|
72
|
+
## Not yet supported
|
|
73
|
+
|
|
74
|
+
WebSocket upgrades under the router are a later task. The adapter's own WebSocket support exists in `src/adapter/` with its tests but is
|
|
75
|
+
not wired to the router yet.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Plugin lifecycle and disposal
|
|
2
|
+
|
|
3
|
+
Cordis gives every framework registration an owner and a lifetime. This is the main difference between using the application API on its
|
|
4
|
+
own and using it as part of ACRYL.
|
|
5
|
+
|
|
6
|
+
## Every registration has an owner
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
export const name = 'todo'
|
|
10
|
+
export const inject = ['router']
|
|
11
|
+
|
|
12
|
+
export function apply(ctx: Context): void {
|
|
13
|
+
ctx.router.route('/todo', createTodoApp())
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`route`, `onError` and `notFound` register themselves as effects of the plugin that called them, so there is nothing to wrap: when the
|
|
18
|
+
plugin's fiber unloads, its mount goes away, and a reload mounts it again without duplicates. `route` returns a function that removes the
|
|
19
|
+
mount sooner. Never register routes as a side effect of importing a module.
|
|
20
|
+
|
|
21
|
+
Resources an app uses (timers, watchers, connections, subprocesses) belong to one `ctx.effect()` of the plugin, whose disposer releases
|
|
22
|
+
them:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
export function apply(ctx: Context): void {
|
|
26
|
+
const events = new EventEmitter()
|
|
27
|
+
ctx.effect(() => {
|
|
28
|
+
const timer = setInterval(() => events.emit('tick'), 1000)
|
|
29
|
+
return () => clearInterval(timer)
|
|
30
|
+
}, 'todo: ticker')
|
|
31
|
+
ctx.router.route('/todo', createTodoApp(events))
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Live composition
|
|
36
|
+
|
|
37
|
+
The router is built for a host whose plugin set changes while it runs. Mounts are added and removed, and each request after a change is
|
|
38
|
+
served by an app composed from the mounts that are live at that time; a request already running keeps the app it started with. The core's
|
|
39
|
+
route tables only grow, so the router rebuilds the composition instead of editing it. Application code does not manage this.
|
|
40
|
+
|
|
41
|
+
A mount is a snapshot of the app's routes at the `route` call. Build the app, then mount it.
|
|
42
|
+
|
|
43
|
+
## Missing providers
|
|
44
|
+
|
|
45
|
+
A plugin that injects `router` stays PENDING while no router exists. That is a normal Cordis state, not an error: when a router appears the
|
|
46
|
+
plugin activates and mounts. When the router is replaced, the plugin unloads and activates again against the new one, so it must not keep
|
|
47
|
+
the old service object anywhere else.
|
|
48
|
+
|
|
49
|
+
## Disposal sequence
|
|
50
|
+
|
|
51
|
+
When a plugin that owns a mount unloads:
|
|
52
|
+
|
|
53
|
+
1. The mount leaves the composition, so new requests no longer reach it.
|
|
54
|
+
2. The requests its code is still serving end: a request inside one of its handlers or middleware (before `next()` returned), and a
|
|
55
|
+
response body one of its handlers produced. Their signals abort (`c.req.raw.signal`) and their bodies are cancelled, which runs the
|
|
56
|
+
stream helpers' `onAbort`.
|
|
57
|
+
3. The unload waits for those requests, their bodies and their `c.executionCtx.waitUntil` work, bounded by the router's `disposeGraceMs`.
|
|
58
|
+
|
|
59
|
+
A request the plugin's middleware already passed on to another plugin's handler is not touched. When the router itself is disposed, the
|
|
60
|
+
same happens to every running request, and new requests are answered with 503.
|
|
61
|
+
|
|
62
|
+
Handlers get a chance to release their resources, nothing a plugin started outlives it, and disposal cannot hang forever.
|
|
63
|
+
|
|
64
|
+
## Services and the shadow context
|
|
65
|
+
|
|
66
|
+
A plugin reaches a service through its own view of it: `ctx.router` in one plugin and in another are two different objects over one
|
|
67
|
+
service. Two facts follow, each with a test as proof.
|
|
68
|
+
|
|
69
|
+
**Fields are shared.** Reading or assigning a service's field through any plugin's view reads or writes the one instance, so every caller
|
|
70
|
+
sees the same value (`plugins/acryl-router/tests/shadow-state.spec.ts`). Keeping changing state in one object, as the router does, is a
|
|
71
|
+
readability choice, not a requirement.
|
|
72
|
+
|
|
73
|
+
**`this.ctx` inside a method is the caller's view, not the service's own context.** It is what lets `route` tie a mount to the plugin
|
|
74
|
+
that called it. It is the wrong context for starting something that should belong to the service: a plugin started with
|
|
75
|
+
`this.ctx.plugin(...)` from a method resolves services as the service's own fiber and fails with "cannot get property router without
|
|
76
|
+
inject". Keep the context the constructor received and use that:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
class Installer extends Service {
|
|
80
|
+
private readonly owner: Context
|
|
81
|
+
|
|
82
|
+
constructor(ctx: Context) {
|
|
83
|
+
super(ctx, 'installer')
|
|
84
|
+
this.owner = ctx // the service's own context
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async install(plugin: Plugin): Promise<void> {
|
|
88
|
+
await this.owner.plugin(plugin) // right: behaves like a plugin mounted at startup
|
|
89
|
+
// await this.ctx.plugin(plugin) // wrong: the caller's view, see above
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The proof is `plugins/acryl-router/tests/live-install.spec.ts`, which runs both forms against a live router. This sits next to the general
|
|
95
|
+
rule: a plugin started with `this.ctx.plugin(...)` in `apply` is a child of the plugin and unloads with it.
|
|
96
|
+
|
|
97
|
+
## Module state
|
|
98
|
+
|
|
99
|
+
Do not hide mutable state in module globals. Cordis can reload a plugin and run several surfaces from one host, and a module global
|
|
100
|
+
outlives the plugin that wrote it.
|
|
101
|
+
|
|
102
|
+
## What to test
|
|
103
|
+
|
|
104
|
+
Test a route-owning plugin through real Cordis activation for:
|
|
105
|
+
|
|
106
|
+
- activation when the router is present, and PENDING until it appears;
|
|
107
|
+
- route removal after the plugin unloads, and a clean mount after reload, without duplicates over repeated cycles;
|
|
108
|
+
- router replacement without stale service references;
|
|
109
|
+
- disposal while a response is still open.
|
|
110
|
+
|
|
111
|
+
`app.request` tests prove handler behavior. Only activation tests prove ownership.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Request
|
|
2
|
+
|
|
3
|
+
Handlers read input through `c.req`, a thin wrapper around the Web `Request`. The original is `c.req.raw`.
|
|
4
|
+
|
|
5
|
+
## Route parameters
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
app.get('/entry/:id', c => {
|
|
9
|
+
const id = c.req.param('id')
|
|
10
|
+
return c.json({ id })
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
app.get('/entry/:id/comment/:commentId', c => {
|
|
14
|
+
const { id, commentId } = c.req.param()
|
|
15
|
+
return c.json({ id, commentId })
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Named reads keep route contracts visible; read the whole map only when a generic logger or adapter needs it.
|
|
20
|
+
|
|
21
|
+
## Query strings
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const page = c.req.query('page') // one value
|
|
25
|
+
const all = c.req.query() // every parameter, first value of each
|
|
26
|
+
const tags = c.req.queries('tag') // repeated values: ?tag=work&tag=home
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Convert strings to numbers, booleans or dates at the application boundary and reject invalid values explicitly; a validator does both
|
|
30
|
+
(see [Validation](./validation.md)).
|
|
31
|
+
|
|
32
|
+
## Headers
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const requestId = c.req.header('X-Request-Id')
|
|
36
|
+
const headers = c.req.header() // every header, keys in lowercase
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Header names are case-insensitive for a named read. With no argument the record's keys are lowercase, so `c.req.header()['X-Foo']` is
|
|
40
|
+
always undefined; read `c.req.header('X-Foo')` instead. Optional headers need a deliberate fallback; do not let a missing authentication
|
|
41
|
+
header silently become an anonymous request unless that is the declared behavior.
|
|
42
|
+
|
|
43
|
+
## Bodies
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
app.post('/entry', async c => {
|
|
47
|
+
const body = await c.req.json() // application/json
|
|
48
|
+
return c.json({ received: body }, 201)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await c.req.text()
|
|
52
|
+
await c.req.arrayBuffer()
|
|
53
|
+
await c.req.blob()
|
|
54
|
+
await c.req.formData()
|
|
55
|
+
await c.req.parseBody() // multipart/form-data or application/x-www-form-urlencoded
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The reads are cached, so a validator and the handler can both read the body. Read untrusted bodies behind `bodyLimit`, which refuses a body
|
|
59
|
+
over the limit with 413 while it streams instead of after buffering it.
|
|
60
|
+
|
|
61
|
+
### Forms and uploads
|
|
62
|
+
|
|
63
|
+
`parseBody()` returns `string | File` per field; the last one wins when a name repeats:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const body = await c.req.parseBody()
|
|
67
|
+
body['file'] // string | File
|
|
68
|
+
const many = await c.req.parseBody({ all: true }) // repeated names become arrays
|
|
69
|
+
many['files'] // (string | File)[] when several were sent
|
|
70
|
+
const nested = await c.req.parseBody({ dot: true }) // obj.key1 -> { obj: { key1 } }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
A field named with a `[]` suffix (`files[]`) is always an array. Treat every uploaded file as untrusted: check its type, size and
|
|
74
|
+
destination, and never use a caller-provided filename as a filesystem path without normalizing it and confining it to the intended
|
|
75
|
+
directory.
|
|
76
|
+
|
|
77
|
+
## Validated input
|
|
78
|
+
|
|
79
|
+
`c.req.valid(target)` returns what a validator for `json`, `form`, `query`, `header`, `cookie` or `param` produced, typed by it. See
|
|
80
|
+
[Validation](./validation.md).
|
|
81
|
+
|
|
82
|
+
## Method, path and URL
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
c.req.method // 'GET'
|
|
86
|
+
c.req.path // '/about/me'
|
|
87
|
+
c.req.url // 'http://127.0.0.1:3290/about/me'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Do not parse the URL by hand to recover route parameters or query values the router already exposes. `routePath(c)` and
|
|
91
|
+
`matchedRoutes(c)` from `acryl-router/helpers` give the registered path and the routes that matched.
|
|
92
|
+
|
|
93
|
+
## Raw Web Request
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
const raw = c.req.raw
|
|
97
|
+
const url = new URL(raw.url)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use the raw object for standards-based libraries, streaming bodies and signatures over the exact payload. Use `c.req` for normal
|
|
101
|
+
application code.
|
|
102
|
+
|
|
103
|
+
## Request cancellation
|
|
104
|
+
|
|
105
|
+
`c.req.raw.signal` aborts when the client goes away, when the plugin serving the request unloads and when the router is
|
|
106
|
+
disposed. Work that ignores it can outlive the plugin that owns it; see [Context](./context.md#cancellation-and-background-work).
|