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,105 @@
|
|
|
1
|
+
# Core concepts
|
|
2
|
+
|
|
3
|
+
The framework's request-handling model is designed so every extension point belongs to a Cordis plugin. This page explains the pieces
|
|
4
|
+
before the API reference.
|
|
5
|
+
|
|
6
|
+
## App
|
|
7
|
+
|
|
8
|
+
An `App` is a self-contained ACRYL application. It owns:
|
|
9
|
+
|
|
10
|
+
- routes;
|
|
11
|
+
- middleware;
|
|
12
|
+
- an error handler (`app.onError`);
|
|
13
|
+
- a request entry point (`app.fetch`, and `app.request` for tests);
|
|
14
|
+
- sub-applications placed below a path with `app.route(path, sub)`.
|
|
15
|
+
|
|
16
|
+
An app can be tested without a server:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const response = await app.request('/todos/42')
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
An app is not itself a Cordis service. That separation lets the same app be mounted by different hosts, at different paths, or not
|
|
23
|
+
mounted at all.
|
|
24
|
+
|
|
25
|
+
## Router service
|
|
26
|
+
|
|
27
|
+
The Cordis `router` service (`ctx.router`, provided by `acryl-router`) composes the mounted apps into one route space. A plugin builds an
|
|
28
|
+
app and mounts it:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
ctx.router.route('/todo', app)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The mount copies the app's routes at that call. When any mount is added or removed, the next request composes a fresh app from the mounts
|
|
35
|
+
that are live, in the order they were made; requests already running keep the app they started with. This matches Cordis: plugins can
|
|
36
|
+
load, unload, reload and be replaced while the host stays up. The core's route tables are add-only, so rebuilding is the only correct way
|
|
37
|
+
to remove a route; it costs about 1 ms per 1000 routes, once per change.
|
|
38
|
+
|
|
39
|
+
The router also takes one router-wide `onError` and one `notFound` (each owned by the plugin that set it), and runs requests in process:
|
|
40
|
+
`ctx.router.fetch(request, connection?)` and `ctx.router.request(path, init?, connection?)`.
|
|
41
|
+
|
|
42
|
+
## Handler
|
|
43
|
+
|
|
44
|
+
A handler receives one context object, conventionally named `c`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
app.get('/todos/:id', c => {
|
|
48
|
+
return c.json({ id: c.req.param('id') })
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Handlers return a Web `Response`, either directly or through context helpers such as `c.text`, `c.json` and `c.html`.
|
|
53
|
+
|
|
54
|
+
## Middleware
|
|
55
|
+
|
|
56
|
+
Middleware wraps handlers and other middleware:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
app.use('/admin/*', async (c, next) => {
|
|
60
|
+
if (!isAllowed(c)) return c.text('Forbidden', 403)
|
|
61
|
+
await next()
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Code before `await next()` runs on the way in. Code after it can inspect or change the response (`c.res`) on the way out.
|
|
66
|
+
|
|
67
|
+
## Surface
|
|
68
|
+
|
|
69
|
+
A surface is the place a person or another program uses the application:
|
|
70
|
+
|
|
71
|
+
| Surface | Transport |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| Web | the ACRYL Host's web server, through `acryl-router-http` |
|
|
74
|
+
| Desktop | the same: the Electron window talks to the app's own loopback web server |
|
|
75
|
+
| Terminal UI | in process: `ctx.router.request(...)`, no server |
|
|
76
|
+
| A standalone process | `acryl-node-server` serves the whole router |
|
|
77
|
+
| Tests | `ctx.router.request(...)` or `app.request(...)` |
|
|
78
|
+
|
|
79
|
+
Surfaces share capabilities and contracts. They do not need to share a transport or a user interface.
|
|
80
|
+
|
|
81
|
+
## Plugin contract
|
|
82
|
+
|
|
83
|
+
Following the ACRYL framework rules, a plugin declares what it:
|
|
84
|
+
|
|
85
|
+
- provides as services;
|
|
86
|
+
- requires through injection;
|
|
87
|
+
- registers as routes, commands, tools or interface slots;
|
|
88
|
+
- owns until disposal.
|
|
89
|
+
|
|
90
|
+
The declaration is what lets tooling answer two questions without reading all the code: what does this plugin do, and what disappears
|
|
91
|
+
when it unloads? `acryl-inspect` reads it from the running system: each mount is labeled with its path and has one labeled child per route it serves.
|
|
92
|
+
|
|
93
|
+
## State
|
|
94
|
+
|
|
95
|
+
Request state belongs to the context. Application state shared between surfaces belongs in `acryl-state` or an application-owned Cordis
|
|
96
|
+
service. Do not hide shared mutable state in module globals: Cordis can reload plugins and run several surfaces from one host, and a
|
|
97
|
+
module global outlives the plugin that wrote it.
|
|
98
|
+
|
|
99
|
+
## Design rules
|
|
100
|
+
|
|
101
|
+
1. Everything extensible is a Cordis plugin.
|
|
102
|
+
2. One application can serve several surfaces.
|
|
103
|
+
3. Each plugin has one declared contract.
|
|
104
|
+
4. Every registration is reversible.
|
|
105
|
+
5. Normal application code should not need runtime internals.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Context
|
|
2
|
+
|
|
3
|
+
Every handler and middleware receives a context, normally named `c`. The context connects the current request, the response being built,
|
|
4
|
+
route data, transport information and work that must finish after the response. It lives for one request.
|
|
5
|
+
|
|
6
|
+
## Return a response
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
app.get('/text', c => c.text('Hello'))
|
|
10
|
+
app.get('/json', c => c.json({ ok: true }))
|
|
11
|
+
app.get('/html', c => c.html('<h1>Hello</h1>'))
|
|
12
|
+
app.get('/empty', c => c.body(null, 204))
|
|
13
|
+
app.get('/raw', () => new Response('Good morning!'))
|
|
14
|
+
app.get('/notfound', c => c.notFound())
|
|
15
|
+
app.get('/redirect', c => c.redirect('/')) // 302
|
|
16
|
+
app.get('/moved', c => c.redirect('/', 301))
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Set the status and headers before or with the body:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
app.post('/posts', c => {
|
|
23
|
+
c.status(201)
|
|
24
|
+
c.header('X-Message', 'created')
|
|
25
|
+
return c.json({ created: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
app.get('/welcome', c => c.body('Thank you for coming', 201, { 'X-Message': 'Hello!', 'Content-Type': 'text/plain' }))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The response on the way out
|
|
32
|
+
|
|
33
|
+
Middleware can read or replace the response after `next()`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
app.use(async (c, next) => {
|
|
37
|
+
await next()
|
|
38
|
+
c.res.headers.append('X-Debug', 'Debug message')
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Read request data
|
|
43
|
+
|
|
44
|
+
The request object is `c.req`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const id = c.req.param('id')
|
|
48
|
+
const search = c.req.query('q')
|
|
49
|
+
const agent = c.req.header('User-Agent')
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See [Request](./request.md) for bodies, repeated values and validated input.
|
|
53
|
+
|
|
54
|
+
## Store request-local values
|
|
55
|
+
|
|
56
|
+
`c.set` and `c.get` (or `c.var.name`) pass data from middleware to handlers. Declare the variables as the app's type parameter so both
|
|
57
|
+
sides agree:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const app = new App<{ Variables: { requestId: string } }>()
|
|
61
|
+
|
|
62
|
+
app.use(async (c, next) => {
|
|
63
|
+
c.set('requestId', crypto.randomUUID())
|
|
64
|
+
await next()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
app.get('/', c => c.text(c.get('requestId')))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
These values live only for the current request. Do not use them as a cache or as application state. `createMiddleware` (from
|
|
71
|
+
`acryl-router/helpers`) types a middleware that sets variables, and chained `.use(...)` calls accumulate their types; see
|
|
72
|
+
[Middleware](./middleware.md).
|
|
73
|
+
|
|
74
|
+
## Transport facts
|
|
75
|
+
|
|
76
|
+
`c.env` carries what the caller of the router supplied for this request. Under the router that is `{ connection }`: the socket's peer
|
|
77
|
+
address and the real local port, filled by the HTTP adapter and empty for an in-process call. Transport facts never travel in headers.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { App, connectionOf, type RouterEnv } from 'acryl-router'
|
|
81
|
+
import { getConnInfo } from 'acryl-router/helpers'
|
|
82
|
+
|
|
83
|
+
const app = new App<RouterEnv>() // types c.env.connection
|
|
84
|
+
app.get('/peer', c => c.json({
|
|
85
|
+
connection: connectionOf(c), // { remoteAddress?, localPort? }
|
|
86
|
+
info: getConnInfo(c), // the same peer in the core's conninfo shape, for middleware such as ipRestriction
|
|
87
|
+
}))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The same handler then runs over HTTP, in process or in a test, and reads an empty connection where there is no socket. Guards such as
|
|
91
|
+
`loopbackOnly` refuse an empty one unless told otherwise.
|
|
92
|
+
|
|
93
|
+
## Cancellation and background work
|
|
94
|
+
|
|
95
|
+
- `c.req.raw.signal` aborts when the caller goes away, when the plugin whose code is serving the request unloads, and when the router is
|
|
96
|
+
disposed. Long work
|
|
97
|
+
should pass it on (`fetch(url, { signal: c.req.raw.signal })`) or check it.
|
|
98
|
+
- `c.executionCtx.waitUntil(promise)` keeps the request in flight until the promise settles, for work that continues after the response
|
|
99
|
+
is returned.
|
|
100
|
+
|
|
101
|
+
Both belong to the current request, and both end with their owner: see [Plugin lifecycle](./plugin-lifecycle.md#disposal-sequence).
|
|
102
|
+
|
|
103
|
+
## Errors and control flow
|
|
104
|
+
|
|
105
|
+
Return a response for expected failures, or throw an `HTTPException`, which becomes its own response:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { HTTPException } from 'acryl-router'
|
|
109
|
+
|
|
110
|
+
if (!todo) return c.json({ error: 'todo_not_found' }, 404)
|
|
111
|
+
if (!allowed) throw new HTTPException(403, { message: 'not yours' })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Anything else that is thrown goes to the app's `onError`, or to the router's when the app has none. A middleware's `await next()` never
|
|
115
|
+
throws: the error has already become a response by then, and the error itself is in `c.error`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
app.use(async (c, next) => {
|
|
119
|
+
await next()
|
|
120
|
+
if (c.error) report(c.error)
|
|
121
|
+
})
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Context rules
|
|
125
|
+
|
|
126
|
+
- Treat the context as request-local.
|
|
127
|
+
- Do not retain it after the handler returns.
|
|
128
|
+
- Do not put sockets, server objects or Cordis internals into context values for other plugins to discover accidentally.
|
|
129
|
+
- Put cross-plugin capabilities in a declared Cordis service, not in `c.set`.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
The ACRYL Framework puts its application model inside Cordis. You build an ordinary `App`, register routes and middleware on it, then
|
|
4
|
+
mount that app from a Cordis plugin.
|
|
5
|
+
|
|
6
|
+
## The mental model
|
|
7
|
+
|
|
8
|
+
There are two layers:
|
|
9
|
+
|
|
10
|
+
1. **Application layer** - `App`, handlers, middleware, context, requests, responses, validation and helpers. This is the ACRYL
|
|
11
|
+
application API.
|
|
12
|
+
2. **Cordis layer** - plugins, services, injection, effects and disposal. This decides when an application exists and which surface can
|
|
13
|
+
reach it.
|
|
14
|
+
|
|
15
|
+
A route therefore has two owners. The `App` owns the route table. The plugin that mounted the app owns the mount for as long as its
|
|
16
|
+
Cordis fiber is active.
|
|
17
|
+
|
|
18
|
+
## A minimal plugin
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
22
|
+
import { App } from 'acryl-router'
|
|
23
|
+
|
|
24
|
+
export const name = 'todo' // equals the package name and the Loader row id
|
|
25
|
+
export const inject = ['router'] // PENDING until a router exists
|
|
26
|
+
|
|
27
|
+
export function apply(ctx: Context): void {
|
|
28
|
+
const app = new App()
|
|
29
|
+
.get('/', c => c.json({ ok: true }))
|
|
30
|
+
.get('/todos/:id', c => c.json({ id: c.req.param('id') }))
|
|
31
|
+
|
|
32
|
+
ctx.router.route('/todo', app)
|
|
33
|
+
// Over HTTP only where the Host's adapter exists; the terminal UI and tests have no such service and skip this.
|
|
34
|
+
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`ctx.router.route('/todo', app)` serves the app below `/todo`:
|
|
39
|
+
|
|
40
|
+
| Request | Answer |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `GET /todo` | `{ "ok": true }` |
|
|
43
|
+
| `GET /todo/todos/42` | `{ "id": "42" }` |
|
|
44
|
+
| `GET /todo/` | 404: `/todo` and `/todo/` are different paths (see below) |
|
|
45
|
+
|
|
46
|
+
### `/todo/` is a 404 until you choose
|
|
47
|
+
|
|
48
|
+
`/todo/` is the first thing people type, and by default it is not found: the router treats `/todo` and `/todo/` as two paths. Choose
|
|
49
|
+
one of these, in this order of preference:
|
|
50
|
+
|
|
51
|
+
- **Redirect in your app** (what `examples/acryl-todo` does): add `trimTrailingSlash()` from `acryl-router/middleware` to the app.
|
|
52
|
+
A GET or HEAD that would be a 404 and ends in `/` gets a 301 to the same path without it, so `/todo/` reaches the page and every
|
|
53
|
+
route keeps one canonical path. Other methods are not redirected: `POST /todo/api/` stays a 404.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { trimTrailingSlash } from 'acryl-router/middleware'
|
|
57
|
+
|
|
58
|
+
const app = new App()
|
|
59
|
+
.use(trimTrailingSlash())
|
|
60
|
+
.get('/', c => c.json({ ok: true }))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- **One path everywhere**: the `acryl-router` row's config `trailingSlash: 'ignore'` makes `/a` and `/a/` the same path for every
|
|
64
|
+
mounted app, with no redirect. It is the app's composition that sets it (the Blueprint or profile that owns the router row), not one
|
|
65
|
+
plugin, because it changes every other plugin's routes too.
|
|
66
|
+
|
|
67
|
+
`new App({ strict: false })` does not help once the app is mounted: the router composes every mount into its own root app, and only the
|
|
68
|
+
router's `trailingSlash` setting decides. `strict: false` applies to an `App` you serve yourself (`app.fetch`, `app.request`). Details
|
|
69
|
+
and a measured table: [Routing and mounting](./routing.md#trailing-slashes).
|
|
70
|
+
|
|
71
|
+
The mount belongs to the plugin that made it: `route` registers it as a Cordis effect of the caller, so when the plugin unloads the
|
|
72
|
+
routes disappear, and a reload mounts the app again without duplicating routes. `route` also returns a function that removes the mount
|
|
73
|
+
sooner.
|
|
74
|
+
|
|
75
|
+
Finish building the app before you mount it: the router copies the app's routes at the `route` call, as the core's own `app.route` does.
|
|
76
|
+
|
|
77
|
+
## Add middleware
|
|
78
|
+
|
|
79
|
+
Middleware is registered on the app before the routes it should wrap:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const app = new App()
|
|
83
|
+
.use(async (c, next) => {
|
|
84
|
+
const started = Date.now()
|
|
85
|
+
await next()
|
|
86
|
+
c.header('X-Response-Time', String(Date.now() - started))
|
|
87
|
+
})
|
|
88
|
+
.get('/', c => c.json({ ok: true }))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Use middleware for authentication, logging, CORS, compression, request IDs and other behavior that should wrap more than one handler.
|
|
92
|
+
The built-in set is listed in [Middleware](./middleware.md).
|
|
93
|
+
|
|
94
|
+
## Run the same app on a surface
|
|
95
|
+
|
|
96
|
+
The application does not open a socket. A surface supplies requests to it:
|
|
97
|
+
|
|
98
|
+
- **Web** and **Desktop**: the Host's web server, through `acryl-router-http` (`routerHttp.mount(prefix)`).
|
|
99
|
+
- **Terminal UI**: in process, `await ctx.router.request('/todo/todos/42')`.
|
|
100
|
+
- **Tests**: `await ctx.router.request(...)` through the plugin, or `await app.request(...)` on the app alone.
|
|
101
|
+
- **A standalone process**: `acryl-node-server`, see [Node server](./node-server.md).
|
|
102
|
+
|
|
103
|
+
## Mount it in a Blend
|
|
104
|
+
|
|
105
|
+
Add a row to the Blend's `cordis.patch.yml`; the row id is the package name:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
- insert:
|
|
109
|
+
- id: todo
|
|
110
|
+
name: todo
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A Blank app already carries the router, the HTTP adapter, state and the inspector. The reference app is `examples/acryl-todo`; the
|
|
114
|
+
smallest one to copy is `examples/acryl-framework-starter`.
|
|
115
|
+
|
|
116
|
+
## Test it without a socket
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
120
|
+
import * as router from 'acryl-router'
|
|
121
|
+
import * as todo from '../src/index.ts'
|
|
122
|
+
|
|
123
|
+
const ctx = new Context()
|
|
124
|
+
ctx.plugin(router)
|
|
125
|
+
ctx.plugin(todo)
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 0)) // let both plugins activate
|
|
127
|
+
const response = await ctx.router.request('/todo/todos/42')
|
|
128
|
+
expect(await response.json()).toEqual({ id: '42' })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Package map
|
|
132
|
+
|
|
133
|
+
| Package | Responsibility |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `acryl-router` | the application API and the Cordis `router` service |
|
|
136
|
+
| `acryl-router-http` | serving router prefixes on the ACRYL Host's web server |
|
|
137
|
+
| `acryl-node-server` | serving the whole router from a standalone Node.js process |
|
|
138
|
+
| `acryl-state` | shared application state across surfaces |
|
|
139
|
+
| `acryl-inspect` | runtime inspection of mounts, routes and plugins |
|
|
140
|
+
|
|
141
|
+
## Next
|
|
142
|
+
|
|
143
|
+
- [Core concepts](./concepts.md)
|
|
144
|
+
- [Routing and mounting](./routing.md)
|
|
145
|
+
- [Context](./context.md)
|
|
146
|
+
- [Plugin lifecycle and disposal](./plugin-lifecycle.md)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Helpers
|
|
2
|
+
|
|
3
|
+
Helpers are small utilities an application should not have to rewrite. They are plain functions over the context, not Cordis services.
|
|
4
|
+
Import them from `acryl-router/helpers`, except the typed client, which has its own entry, `acryl-router/client`.
|
|
5
|
+
|
|
6
|
+
| Helper | Use |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| `stream`, `streamText`, `streamSSE` | streaming responses and server-sent events |
|
|
9
|
+
| `getCookie`, `setCookie`, `deleteCookie`, `getSignedCookie`, `setSignedCookie`, `generateCookie`, `generateSignedCookie` | cookies |
|
|
10
|
+
| `testClient` | a typed client that calls an app in process, for tests |
|
|
11
|
+
| `validator`, `schemaValidator` | input validation, see [Validation](./validation.md) |
|
|
12
|
+
| `createMiddleware`, `createFactory` | typed middleware and handlers that share variables |
|
|
13
|
+
| `html`, `raw` | escaped HTML templates |
|
|
14
|
+
| `accepts` | content negotiation from `Accept` headers |
|
|
15
|
+
| `routePath`, `matchedRoutes`, `baseRoutePath`, `basePath` | the route a request matched |
|
|
16
|
+
| `getConnInfo`, `connectionOf` | the peer and local port, see [Context](./context.md#transport-facts) |
|
|
17
|
+
| `getMimeType`, `getExtension`, `mimes` | media types by file extension |
|
|
18
|
+
| `defineWebSocketHelper`, `WSContext` and their types | the runtime-neutral WebSocket pieces an adapter builds on |
|
|
19
|
+
|
|
20
|
+
JWT signing and verification (`sign`, `verify`, `decode`) live with the `jwt` middleware in `acryl-router/middleware`.
|
|
21
|
+
|
|
22
|
+
## Streaming
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { stream, streamSSE, streamText } from 'acryl-router/helpers'
|
|
26
|
+
|
|
27
|
+
app.get('/stream', c => stream(c, async stream => {
|
|
28
|
+
stream.onAbort(() => console.log('aborted'))
|
|
29
|
+
await stream.write(new Uint8Array([0x48, 0x69]))
|
|
30
|
+
await stream.pipe(anotherReadableStream)
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
app.get('/text', c => streamText(c, async stream => {
|
|
34
|
+
await stream.writeln('Hello')
|
|
35
|
+
await stream.sleep(1000)
|
|
36
|
+
await stream.write('there')
|
|
37
|
+
}))
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Server-sent events write one message at a time with `writeSSE`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
app.get('/events', c => streamSSE(c, async stream => {
|
|
44
|
+
let id = 0
|
|
45
|
+
while (!stream.aborted) {
|
|
46
|
+
await stream.writeSSE({ event: 'time', data: new Date().toISOString(), id: String(id++) })
|
|
47
|
+
await stream.sleep(1000)
|
|
48
|
+
}
|
|
49
|
+
}))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- The callback runs after the response is returned. When it throws, the optional third argument `onError(error, stream)` runs and the
|
|
53
|
+
stream sends an `error` event before it closes.
|
|
54
|
+
- There is no built-in keep-alive: send a comment or an event on a timer when a proxy closes idle connections.
|
|
55
|
+
- When the plugin that serves the stream unloads, or the router is disposed, the router cancels the response body, so `onAbort` runs,
|
|
56
|
+
`aborted` turns true and a loop that checks `stream.aborted` ends with its owner.
|
|
57
|
+
|
|
58
|
+
## Cookies
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { deleteCookie, getCookie, getSignedCookie, setCookie, setSignedCookie } from 'acryl-router/helpers'
|
|
62
|
+
|
|
63
|
+
app.get('/cookie', c => {
|
|
64
|
+
setCookie(c, 'theme', 'dark', { path: '/', httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 60 * 60 * 24 })
|
|
65
|
+
const theme = getCookie(c, 'theme')
|
|
66
|
+
const all = getCookie(c)
|
|
67
|
+
deleteCookie(c, 'old')
|
|
68
|
+
return c.json({ theme, all })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
app.get('/signed', async c => {
|
|
72
|
+
await setSignedCookie(c, 'session', 'value', secret)
|
|
73
|
+
const session = await getSignedCookie(c, secret, 'session') // false when the signature does not match
|
|
74
|
+
return c.json({ session })
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`generateCookie` and `generateSignedCookie` return the `Set-Cookie` string without touching a context. The `__Secure-` and `__Host-`
|
|
79
|
+
prefixes are available through `{ prefix: 'secure' }` and `{ prefix: 'host' }`, and the options are checked against their rules.
|
|
80
|
+
|
|
81
|
+
## Testing
|
|
82
|
+
|
|
83
|
+
`app.request(path, init?)` calls an app in process and returns a `Response`. `testClient` adds the route types:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { testClient } from 'acryl-router/helpers'
|
|
87
|
+
|
|
88
|
+
const app = new App()
|
|
89
|
+
.get('/search', c => c.json({ query: c.req.query('q') }))
|
|
90
|
+
|
|
91
|
+
const client = testClient(app)
|
|
92
|
+
const response = await client.search.$get({ query: { q: 'cordis' } })
|
|
93
|
+
expect(await response.json()).toEqual({ query: 'cordis' })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The routes must be chained on one expression (as above) for their types to reach the client. Add a test through the Cordis router as well:
|
|
97
|
+
an app test cannot prove the mount, the prefix or the disposal.
|
|
98
|
+
|
|
99
|
+
## The typed client
|
|
100
|
+
|
|
101
|
+
`acryl-router/client` is the same client for a real server. It runs in a browser or any runtime with `fetch`, without Cordis:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { hc } from 'acryl-router/client'
|
|
105
|
+
import type { SearchRoutes } from 'my-server' // the type of the chained app, exported by the server package
|
|
106
|
+
|
|
107
|
+
const client = hc<SearchRoutes>('http://127.0.0.1:3290/search-app')
|
|
108
|
+
const response = await client.search.$get({ query: { q: 'cordis' } })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Export the app's type (`export type SearchRoutes = typeof app`) from the server package; the client imports only the type, never the
|
|
112
|
+
server code.
|
|
113
|
+
|
|
114
|
+
## Not yet ported
|
|
115
|
+
|
|
116
|
+
Proxying, a WebSocket upgrade under the router, JSX, static-site generation, CSS-in-JS and the cache helpers are later tasks (see
|
|
117
|
+
`specs/047-framework-port/plan.md`). Add a missing helper to the framework as a ported, tested helper, not as private code in one
|
|
118
|
+
application.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Middleware
|
|
2
|
+
|
|
3
|
+
Middleware is code that runs around a handler. Use it for behavior shared by several routes: authentication, authorization, logging, CORS,
|
|
4
|
+
caching, compression, request IDs and timing.
|
|
5
|
+
|
|
6
|
+
## Write middleware
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import type { MiddlewareHandler } from 'acryl-router'
|
|
10
|
+
|
|
11
|
+
const timing: MiddlewareHandler = async (c, next) => {
|
|
12
|
+
const started = performance.now()
|
|
13
|
+
await next()
|
|
14
|
+
c.header('X-Response-Time', String(Math.round(performance.now() - started)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
app.use(timing)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`await next()` runs the remaining middleware and then the handler. Not calling it ends the chain, so middleware that refuses a request
|
|
21
|
+
returns a response itself:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const requireAdmin: MiddlewareHandler = async (c, next) => {
|
|
25
|
+
if (!isAdmin(c)) return c.json({ error: 'forbidden' }, 403)
|
|
26
|
+
await next()
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Middleware does not return a response *and* call `next()`: after `next()` it changes `c.res` (or assigns a new `Response` to it) instead.
|
|
31
|
+
|
|
32
|
+
Authentication establishes an identity; authorization decides whether that identity may do this. Keep them as separate middleware when
|
|
33
|
+
their failure answers differ.
|
|
34
|
+
|
|
35
|
+
## Scope and order
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
app.use(requestLogger) // every route of this app
|
|
39
|
+
app.use('/admin/*', requireAdmin) // below a prefix
|
|
40
|
+
app.on(['POST', 'PUT'], '/items/*', audit)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Middleware runs in registration order and only wraps routes registered after it. The first middleware registered is the outermost: its code
|
|
44
|
+
before `next()` runs first and its code after `next()` runs last.
|
|
45
|
+
|
|
46
|
+
## Share values with handlers
|
|
47
|
+
|
|
48
|
+
`createMiddleware` types the variables a middleware sets, and the app picks them up when it is chained:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createMiddleware } from 'acryl-router/helpers'
|
|
52
|
+
|
|
53
|
+
const withUser = createMiddleware<{ Variables: { user: User } }>(async (c, next) => {
|
|
54
|
+
c.set('user', await loadUser(c))
|
|
55
|
+
await next()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const app = new App().use(withUser).get('/me', c => c.json(c.var.user))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Only request-local values belong here. Shared services belong behind Cordis injection.
|
|
62
|
+
|
|
63
|
+
## Errors
|
|
64
|
+
|
|
65
|
+
`await next()` does not throw. When a handler or later middleware throws, the error becomes a response first (an `HTTPException` its own,
|
|
66
|
+
anything else through `onError`) and is left in `c.error`:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
app.use(async (c, next) => {
|
|
70
|
+
await next()
|
|
71
|
+
if (c.error) metrics.count('request_failed')
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
So a `try { await next() } catch` around `next()` never catches anything. Map domain errors to answers in `app.onError`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
app.onError((error, c) => {
|
|
79
|
+
if (error instanceof DomainError) return c.json({ error: error.code }, error.status)
|
|
80
|
+
console.error(error)
|
|
81
|
+
return c.json({ error: 'internal_error' }, 500)
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Built-in middleware
|
|
86
|
+
|
|
87
|
+
Import from `acryl-router/middleware`:
|
|
88
|
+
|
|
89
|
+
| Middleware | What it does |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `basicAuth`, `bearerAuth` | HTTP Basic and Bearer authentication |
|
|
92
|
+
| `jwt`, `jwk` (with `sign`, `verify`, `decode`, `verifyWithJwks`) | JSON Web Token verification, from a secret or a key set |
|
|
93
|
+
| `bodyLimit` | refuse a request body over a size, with 413, while it streams |
|
|
94
|
+
| `cors` | CORS headers and preflight answers |
|
|
95
|
+
| `csrf` | refuse a cross-site form submission by its `Origin` or `Sec-Fetch-Site` |
|
|
96
|
+
| `secureHeaders` (with `NONCE`) | security headers, including a content security policy with nonces |
|
|
97
|
+
| `ipRestriction` | allow or deny by the peer address (read through `getConnInfo`) |
|
|
98
|
+
| `loopbackOnly` (with `loopbackAuthority`, `sameOrigin`) | only requests from this machine to this app; see below |
|
|
99
|
+
| `compress` | gzip or deflate the response |
|
|
100
|
+
| `etag` | ETag headers and 304 answers |
|
|
101
|
+
| `logger` | one log line per request and response |
|
|
102
|
+
| `requestId` | a request ID header and `c.var.requestId` |
|
|
103
|
+
| `timing` (with `startTime`, `endTime`, `setMetric`, `wrapTime`) | `Server-Timing` headers |
|
|
104
|
+
| `timeout` | answer with an error when a handler takes too long |
|
|
105
|
+
| `prettyJSON` | indented JSON when the query asks for it |
|
|
106
|
+
| `poweredBy` | an `X-Powered-By` header |
|
|
107
|
+
| `methodNotAllowed({ app })` | 405 with an `Allow` header for a known path of `app` asked with another method |
|
|
108
|
+
| `methodOverride` | take the method from a form field, header or query |
|
|
109
|
+
| `trimTrailingSlash`, `appendTrailingSlash` | redirect to the canonical path |
|
|
110
|
+
| `languageDetector` (with `detectFromPath`, `detectFromQuery`, `detectFromCookie`, `detectFromHeader`) | the request's language as a variable |
|
|
111
|
+
| `contextStorage` (with `getContext`, `tryGetContext`) | the current context from code that is not given `c` |
|
|
112
|
+
| `every`, `some`, `except` | combine middleware: all, the first that passes, or all except on some paths |
|
|
113
|
+
| `serveStatic` | the runtime-neutral static file middleware; it needs a `getContent`. On Node use the one from `acryl-node-server` |
|
|
114
|
+
|
|
115
|
+
### loopbackOnly
|
|
116
|
+
|
|
117
|
+
The guard for a local app's own routes, such as a control API a desktop window calls:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { loopbackOnly } from 'acryl-router/middleware'
|
|
121
|
+
|
|
122
|
+
app.use('/control/*', loopbackOnly())
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A request passes only when:
|
|
126
|
+
|
|
127
|
+
- the peer is a loopback address and arrived on the real local port, both read from the transport, never from a header;
|
|
128
|
+
- the `Host` header names a loopback hostname (`127.0.0.1`, `localhost`, `[::1]`, or `hostnames`) with that port, which stops DNS
|
|
129
|
+
rebinding;
|
|
130
|
+
- for a request that changes things (not `GET`, `HEAD`, `OPTIONS`), any `Origin` is this same authority (`mutations: 'none'` turns this
|
|
131
|
+
off).
|
|
132
|
+
|
|
133
|
+
An in-process call has no socket and is refused unless `inProcess: 'allow'` says the host's own code may call it. Everything refused gets a
|
|
134
|
+
403.
|
|
135
|
+
|
|
136
|
+
A private route that no other program on the machine may call either, such as a workspace's file and terminal routes, takes
|
|
137
|
+
`callers: 'own-page'`:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
app.use('/api/workspace/*', loopbackOnly({ callers: 'own-page', hostnames: ['127.0.0.1'] }))
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Then every request must come from a page this app served: an `Origin` that is exactly this authority (with `Sec-Fetch-Site` absent or
|
|
144
|
+
`same-origin`), or, for a read, which browsers often send without `Origin`, `Sec-Fetch-Site: same-origin` and a `Referer` from this
|
|
145
|
+
authority. A script with no `Origin` is refused even for a read. `fromOwnPage(c, authority)` is the same check for middleware of your own.
|
|
146
|
+
|
|
147
|
+
## Middleware in Cordis
|
|
148
|
+
|
|
149
|
+
Middleware belongs to an `App`; it is not a separate Cordis registration, and it disappears with the mount that serves it. A plugin may
|
|
150
|
+
build middleware from an injected service, but resources the middleware uses (timers, watchers, connections) are acquired in the plugin's
|
|
151
|
+
`ctx.effect()` and only the finished middleware goes into the app. Do not start timers, watchers or sockets from a handler.
|
|
152
|
+
|
|
153
|
+
Middleware an app registers with a path wraps the matching routes of every later mount too (an app mounted at `/` with
|
|
154
|
+
`app.use('/todo/*', ...)` wraps a plugin mounted at `/todo`), so a host can put a guard in front of other plugins by mounting first.
|