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
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { Middleware, RouteContext } from '../types.ts'
|
|
2
|
-
|
|
3
|
-
export interface BodyLimitOptions {
|
|
4
|
-
readonly maxBytes: number
|
|
5
|
-
/** The response for a body that is too large. Defaults to a 413 with a small JSON body. */
|
|
6
|
-
readonly onError?: (c: RouteContext) => Response | Promise<Response>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** The context key under which the limit is kept for the body-reading helpers. */
|
|
10
|
-
export const BODY_LIMIT_KEY = 'bodyLimit'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Refuse a request whose declared `content-length` is over the limit, and remember the limit so `readJson` and `readText` enforce it on the bytes
|
|
14
|
-
* they read (a chunked body declares no length). A request body is a stream: only the helpers, or a handler that counts, can stop it midway.
|
|
15
|
-
*/
|
|
16
|
-
export function bodyLimit(options: BodyLimitOptions): Middleware {
|
|
17
|
-
return async (c, next) => {
|
|
18
|
-
const declared = Number(c.req.headers.get('content-length'))
|
|
19
|
-
if (Number.isFinite(declared) && declared > options.maxBytes) {
|
|
20
|
-
return options.onError === undefined ? c.json({ error: 'payload too large', maxBytes: options.maxBytes }, 413) : options.onError(c)
|
|
21
|
-
}
|
|
22
|
-
c.set(BODY_LIMIT_KEY, options.maxBytes)
|
|
23
|
-
return next()
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from '../types.ts'
|
|
2
|
-
import { appendVary, setHeaders } from '../util.ts'
|
|
3
|
-
|
|
4
|
-
export interface CorsOptions {
|
|
5
|
-
/** `'*'`, one origin, a list, or a function that returns the origin to allow (or `undefined` to refuse). */
|
|
6
|
-
readonly origin?: string | readonly string[] | ((origin: string) => string | undefined)
|
|
7
|
-
readonly allowMethods?: readonly string[]
|
|
8
|
-
readonly allowHeaders?: readonly string[]
|
|
9
|
-
readonly exposeHeaders?: readonly string[]
|
|
10
|
-
readonly maxAge?: number
|
|
11
|
-
readonly credentials?: boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DEFAULT_METHODS = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
|
|
15
|
-
|
|
16
|
-
/** Cross-origin resource sharing, with preflight answered here (a 204 that never reaches the route). */
|
|
17
|
-
export function cors(options: CorsOptions = {}): Middleware {
|
|
18
|
-
const allowed = (origin: string): string | undefined => {
|
|
19
|
-
const rule = options.origin ?? '*'
|
|
20
|
-
if (typeof rule === 'function') return rule(origin)
|
|
21
|
-
if (typeof rule === 'string') return rule === '*' || rule === origin ? rule : undefined
|
|
22
|
-
return rule.includes(origin) ? origin : undefined
|
|
23
|
-
}
|
|
24
|
-
return async (c, next) => {
|
|
25
|
-
const origin = c.req.headers.get('origin')
|
|
26
|
-
const match = origin === null ? undefined : allowed(origin)
|
|
27
|
-
const shared: Record<string, string> = {}
|
|
28
|
-
if (match !== undefined) shared['access-control-allow-origin'] = match
|
|
29
|
-
if (options.credentials === true && match !== undefined) shared['access-control-allow-credentials'] = 'true'
|
|
30
|
-
if (options.exposeHeaders !== undefined && options.exposeHeaders.length > 0) shared['access-control-expose-headers'] = options.exposeHeaders.join(',')
|
|
31
|
-
|
|
32
|
-
if (c.req.method === 'OPTIONS' && c.req.headers.has('access-control-request-method')) {
|
|
33
|
-
const headers = { ...shared }
|
|
34
|
-
if (match !== undefined) {
|
|
35
|
-
headers['access-control-allow-methods'] = (options.allowMethods ?? DEFAULT_METHODS).join(',')
|
|
36
|
-
const requested = c.req.headers.get('access-control-request-headers')
|
|
37
|
-
const allowHeaders = options.allowHeaders ?? (requested === null ? [] : requested.split(',').map(part => part.trim()))
|
|
38
|
-
if (allowHeaders.length > 0) headers['access-control-allow-headers'] = allowHeaders.join(',')
|
|
39
|
-
if (options.maxAge !== undefined) headers['access-control-max-age'] = String(options.maxAge)
|
|
40
|
-
}
|
|
41
|
-
return appendVary(c.body(null, 204, headers), 'Origin')
|
|
42
|
-
}
|
|
43
|
-
const response = await next()
|
|
44
|
-
return appendVary(setHeaders(response, shared), 'Origin')
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from '../types.ts'
|
|
2
|
-
|
|
3
|
-
/** One line to a sink the caller owns, for example `line => ctx.logger.info(line)`. */
|
|
4
|
-
export type LogSink = (line: string) => void
|
|
5
|
-
|
|
6
|
-
/** Log each request on the way in and its status and time on the way out. The sink is required: a framework does not write to the console on its own. */
|
|
7
|
-
export function logger(sink: LogSink): Middleware {
|
|
8
|
-
return async (c, next) => {
|
|
9
|
-
const label = `${c.req.method} ${c.url.pathname}`
|
|
10
|
-
sink(`<-- ${label}`)
|
|
11
|
-
const started = performance.now()
|
|
12
|
-
try {
|
|
13
|
-
const response = await next()
|
|
14
|
-
sink(`--> ${label} ${String(response.status)} ${String(Math.round(performance.now() - started))}ms`)
|
|
15
|
-
return response
|
|
16
|
-
} catch (error) {
|
|
17
|
-
sink(`--> ${label} threw ${error instanceof Error ? error.message : String(error)} ${String(Math.round(performance.now() - started))}ms`)
|
|
18
|
-
throw error
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto'
|
|
2
|
-
import type { Middleware } from '../types.ts'
|
|
3
|
-
import { setHeaders } from '../util.ts'
|
|
4
|
-
|
|
5
|
-
export interface RequestIdOptions {
|
|
6
|
-
/** The header read from the request and written to the response. */
|
|
7
|
-
readonly header?: string
|
|
8
|
-
/** Makes an id when the request brought none. */
|
|
9
|
-
readonly generate?: () => string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const SAFE_ID = /^[\w\-=]{1,255}$/u
|
|
13
|
-
|
|
14
|
-
/** Give every request an id: the caller's, when it is well formed, otherwise a new one. It is `c.get('requestId')` and the response header. */
|
|
15
|
-
export function requestId(options: RequestIdOptions = {}): Middleware {
|
|
16
|
-
const header = options.header ?? 'x-request-id'
|
|
17
|
-
const generate = options.generate ?? randomUUID
|
|
18
|
-
return async (c, next) => {
|
|
19
|
-
const incoming = c.req.headers.get(header)
|
|
20
|
-
const id = incoming !== null && SAFE_ID.test(incoming) ? incoming : generate()
|
|
21
|
-
c.set('requestId', id)
|
|
22
|
-
return setHeaders(await next(), { [header]: id })
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Middleware } from '../types.ts'
|
|
2
|
-
import { setHeaders } from '../util.ts'
|
|
3
|
-
|
|
4
|
-
export interface SecureHeadersOptions {
|
|
5
|
-
/** Extra or replacement headers, merged over the defaults. A value of `''` removes a default. */
|
|
6
|
-
readonly headers?: Readonly<Record<string, string>>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const DEFAULTS: Record<string, string> = {
|
|
10
|
-
'x-content-type-options': 'nosniff',
|
|
11
|
-
'x-frame-options': 'SAMEORIGIN',
|
|
12
|
-
'referrer-policy': 'no-referrer',
|
|
13
|
-
'cross-origin-resource-policy': 'same-origin',
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Add the standard hardening headers to every response (never overriding one the handler already set). */
|
|
17
|
-
export function secureHeaders(options: SecureHeadersOptions = {}): Middleware {
|
|
18
|
-
const wanted = Object.entries({ ...DEFAULTS, ...options.headers }).filter(([, value]) => value !== '')
|
|
19
|
-
return async (_c, next) => {
|
|
20
|
-
const response = await next()
|
|
21
|
-
return setHeaders(response, Object.fromEntries(wanted.filter(([name]) => !response.headers.has(name))))
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { TrailingSlash } from './types.ts'
|
|
2
|
-
|
|
3
|
-
/** One piece of a route path. */
|
|
4
|
-
export type Segment =
|
|
5
|
-
| { readonly kind: 'static', readonly value: string }
|
|
6
|
-
| { readonly kind: 'param', readonly name: string }
|
|
7
|
-
| { readonly kind: 'wildcard' }
|
|
8
|
-
|
|
9
|
-
export interface Pattern {
|
|
10
|
-
/** The path as written (normalized for the trailing-slash rule). */
|
|
11
|
-
readonly source: string
|
|
12
|
-
readonly segments: readonly Segment[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Split a pathname into segments. `/` is the empty list; under `strict` a trailing slash leaves a final empty segment. */
|
|
16
|
-
export function splitPath(path: string, trailingSlash: TrailingSlash): string[] {
|
|
17
|
-
let value = path
|
|
18
|
-
if (trailingSlash === 'ignore' && value.length > 1 && value.endsWith('/')) value = value.slice(0, -1)
|
|
19
|
-
if (value === '/' || value === '') return []
|
|
20
|
-
return value.slice(1).split('/')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Compile `/a/:id/*`. A `*` is allowed only as the last segment; param names must be non-empty and unique. */
|
|
24
|
-
export function compilePattern(path: string, trailingSlash: TrailingSlash): Pattern {
|
|
25
|
-
if (!path.startsWith('/')) throw new TypeError(`router: path "${path}" must start with "/"`)
|
|
26
|
-
const parts = splitPath(path, trailingSlash)
|
|
27
|
-
const names = new Set<string>()
|
|
28
|
-
const segments: Segment[] = parts.map((part, index) => {
|
|
29
|
-
if (part === '*') {
|
|
30
|
-
if (index !== parts.length - 1) throw new TypeError(`router: "*" must be the last segment in "${path}"`)
|
|
31
|
-
return { kind: 'wildcard' }
|
|
32
|
-
}
|
|
33
|
-
if (part.startsWith(':')) {
|
|
34
|
-
const name = part.slice(1)
|
|
35
|
-
if (name === '') throw new TypeError(`router: empty parameter name in "${path}"`)
|
|
36
|
-
if (names.has(name)) throw new TypeError(`router: duplicate parameter ":${name}" in "${path}"`)
|
|
37
|
-
names.add(name)
|
|
38
|
-
return { kind: 'param', name }
|
|
39
|
-
}
|
|
40
|
-
return { kind: 'static', value: part }
|
|
41
|
-
})
|
|
42
|
-
return { source: path, segments }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The same path with every parameter name erased, so `/a/:id` and `/a/:name` collide as duplicates. */
|
|
46
|
-
export function shapeOf(pattern: Pattern): string {
|
|
47
|
-
return '/' + pattern.segments.map(s => s.kind === 'static' ? s.value : s.kind === 'param' ? ':' : '*').join('/')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function decode(value: string): string {
|
|
51
|
-
try { return decodeURIComponent(value) } catch { return value }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Match concrete path segments against a pattern; returns the parameters, or `undefined` when it does not match. */
|
|
55
|
-
export function matchPattern(pattern: Pattern, parts: readonly string[]): Record<string, string> | undefined {
|
|
56
|
-
const params: Record<string, string> = {}
|
|
57
|
-
const { segments } = pattern
|
|
58
|
-
for (let i = 0; i < segments.length; i++) {
|
|
59
|
-
const segment = segments[i]
|
|
60
|
-
if (segment === undefined) return undefined
|
|
61
|
-
if (segment.kind === 'wildcard') {
|
|
62
|
-
params['*'] = parts.slice(i).map(decode).join('/')
|
|
63
|
-
return params
|
|
64
|
-
}
|
|
65
|
-
const part = parts[i]
|
|
66
|
-
if (part === undefined) return undefined
|
|
67
|
-
if (segment.kind === 'static') {
|
|
68
|
-
if (segment.value !== part) return undefined
|
|
69
|
-
} else {
|
|
70
|
-
params[segment.name] = decode(part)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return parts.length === segments.length ? params : undefined
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const SCORE = { static: 3, param: 2, wildcard: 1 } as const
|
|
77
|
-
|
|
78
|
-
/** Compare two matching patterns: the more specific one (static before param before wildcard, left to right) wins. */
|
|
79
|
-
export function compareSpecificity(a: Pattern, b: Pattern): number {
|
|
80
|
-
const length = Math.max(a.segments.length, b.segments.length)
|
|
81
|
-
for (let i = 0; i < length; i++) {
|
|
82
|
-
const left = a.segments[i]
|
|
83
|
-
const right = b.segments[i]
|
|
84
|
-
const l = left === undefined ? 0 : SCORE[left.kind]
|
|
85
|
-
const r = right === undefined ? 0 : SCORE[right.kind]
|
|
86
|
-
if (l !== r) return l - r
|
|
87
|
-
}
|
|
88
|
-
return 0
|
|
89
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The registration API every router scope shares (`get`, `use`, `group`): the router itself is one scope with an empty prefix, and
|
|
3
|
-
* `router.group('/api')` is another with a prefix. A scope never owns anything; every registration is released by the plugin that made it.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
7
|
-
import { compose } from './compose.ts'
|
|
8
|
-
import type { Handler, Method, Middleware, RouteMethod } from './types.ts'
|
|
9
|
-
|
|
10
|
-
/** The last argument is the handler; everything before it is middleware that runs for this route only. */
|
|
11
|
-
export type RouteHandlers = readonly [...Middleware[], Handler]
|
|
12
|
-
|
|
13
|
-
export type UseArguments = readonly [Middleware, ...Middleware[]] | readonly [string, Middleware, ...Middleware[]]
|
|
14
|
-
|
|
15
|
-
export interface Registrar {
|
|
16
|
-
/** Register a route owned by the calling plugin. Returns a remover; the route also goes away when the caller unloads. */
|
|
17
|
-
route(method: RouteMethod, path: string, ...handlers: RouteHandlers): () => void
|
|
18
|
-
get(path: string, ...handlers: RouteHandlers): () => void
|
|
19
|
-
post(path: string, ...handlers: RouteHandlers): () => void
|
|
20
|
-
put(path: string, ...handlers: RouteHandlers): () => void
|
|
21
|
-
patch(path: string, ...handlers: RouteHandlers): () => void
|
|
22
|
-
delete(path: string, ...handlers: RouteHandlers): () => void
|
|
23
|
-
/** A route that answers every method no more specific route claims. */
|
|
24
|
-
all(path: string, ...handlers: RouteHandlers): () => void
|
|
25
|
-
/** The same handlers on several methods. Returns one remover for all of them. */
|
|
26
|
-
on(methods: Method | readonly Method[], path: string, ...handlers: RouteHandlers): () => void
|
|
27
|
-
/** Middleware for a path and everything below it (default: everything in this scope), in registration order. */
|
|
28
|
-
use(...args: UseArguments): () => void
|
|
29
|
-
/** A scope whose paths all start with `prefix`; optional middleware runs for everything under it. */
|
|
30
|
-
group(prefix: string, ...middleware: Middleware[]): Registrar
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** What a scope needs from the router: put one route or one middleware into the table, owned by `owner`. */
|
|
34
|
-
export interface Primitives {
|
|
35
|
-
add(owner: Context, method: RouteMethod, path: string, handler: Handler): () => void
|
|
36
|
-
use(owner: Context, path: string, middleware: Middleware): () => void
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const PREFIX = /^\/(?:[^/*:\s]+(?:\/[^/*:\s]+)*)?$/
|
|
40
|
-
|
|
41
|
-
/** Join a scope prefix and a path: `('/api', '/x')` is `/api/x`, `('/api', '/')` is `/api`, and an empty prefix leaves the path alone. */
|
|
42
|
-
export function joinPath(prefix: string, path: string): string {
|
|
43
|
-
if (prefix === '') return path
|
|
44
|
-
if (path === '/' || path === '') return prefix
|
|
45
|
-
if (!path.startsWith('/')) throw new TypeError(`router: path "${path}" must start with "/"`)
|
|
46
|
-
return `${prefix}${path}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function validatePrefix(prefix: string): void {
|
|
50
|
-
if (prefix !== '' && !PREFIX.test(prefix)) throw new TypeError(`router: group prefix "${prefix}" must be a plain path like "/api" (no wildcard, no parameter, no trailing slash)`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function single(handlers: RouteHandlers): Handler {
|
|
54
|
-
const last = handlers.at(-1)
|
|
55
|
-
if (last === undefined) throw new TypeError('router: a route needs a handler')
|
|
56
|
-
const middleware = handlers.slice(0, -1) as Middleware[]
|
|
57
|
-
return middleware.length === 0 ? last as Handler : compose(middleware, last as Handler)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function createRegistrar(owner: Context, prefix: string, primitives: Primitives): Registrar {
|
|
61
|
-
validatePrefix(prefix)
|
|
62
|
-
const route = (method: RouteMethod, path: string, ...handlers: RouteHandlers): (() => void) =>
|
|
63
|
-
primitives.add(owner, method, joinPath(prefix, path), single(handlers))
|
|
64
|
-
const registrar: Registrar = {
|
|
65
|
-
route,
|
|
66
|
-
get: (path, ...h) => route('GET', path, ...h),
|
|
67
|
-
post: (path, ...h) => route('POST', path, ...h),
|
|
68
|
-
put: (path, ...h) => route('PUT', path, ...h),
|
|
69
|
-
patch: (path, ...h) => route('PATCH', path, ...h),
|
|
70
|
-
delete: (path, ...h) => route('DELETE', path, ...h),
|
|
71
|
-
all: (path, ...h) => route('*', path, ...h),
|
|
72
|
-
on: (methods, path, ...h) => {
|
|
73
|
-
const removers = (Array.isArray(methods) ? methods as readonly Method[] : [methods as Method]).map(method => route(method, path, ...h))
|
|
74
|
-
return () => { for (const remove of removers) remove() }
|
|
75
|
-
},
|
|
76
|
-
use: (...args) => {
|
|
77
|
-
const [first, ...rest] = args as readonly [string | Middleware, ...Middleware[]]
|
|
78
|
-
const path = typeof first === 'string' ? first : '/'
|
|
79
|
-
const middleware = typeof first === 'string' ? rest : [first, ...rest]
|
|
80
|
-
const removers = middleware.map(mw => primitives.use(owner, joinPath(prefix, path), mw))
|
|
81
|
-
return () => { for (const remove of removers) remove() }
|
|
82
|
-
},
|
|
83
|
-
group: (nested, ...middleware) => {
|
|
84
|
-
const scope = createRegistrar(owner, joinPath(prefix, nested), primitives)
|
|
85
|
-
if (middleware.length > 0) scope.use(...(middleware as [Middleware, ...Middleware[]]))
|
|
86
|
-
return scope
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
return registrar
|
|
90
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { compose } from './compose.ts'
|
|
2
|
-
import { createContext } from './context.ts'
|
|
3
|
-
import { compareSpecificity, compilePattern, matchPattern, shapeOf, splitPath, type Pattern } from './pattern.ts'
|
|
4
|
-
import { HttpError } from './http-error.ts'
|
|
5
|
-
import type { ConnectionInfo, ErrorHandler, Handler, Middleware, NotFoundHandler, RouteContext, RouteMethod, TrailingSlash } from './types.ts'
|
|
6
|
-
|
|
7
|
-
interface RouteEntry {
|
|
8
|
-
readonly method: RouteMethod
|
|
9
|
-
readonly pattern: Pattern
|
|
10
|
-
readonly handler: Handler
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface MiddlewareEntry {
|
|
14
|
-
readonly pattern: Pattern
|
|
15
|
-
readonly middleware: Middleware
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface RouteTableOptions {
|
|
19
|
-
readonly trailingSlash: TrailingSlash
|
|
20
|
-
/** Called with anything a handler or middleware throws; the caller still receives a 500. */
|
|
21
|
-
readonly onError?: (error: unknown) => void
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The routing core: pure data and matching, no Cordis. Registrations return an idempotent remover, and the table keeps
|
|
26
|
-
* insertion order so middleware runs in registration order. Matching picks the most specific route (static before
|
|
27
|
-
* parameter before wildcard) among those that answer the request method.
|
|
28
|
-
*/
|
|
29
|
-
export class RouteTable {
|
|
30
|
-
private readonly routes = new Map<string, RouteEntry>()
|
|
31
|
-
private readonly middleware = new Map<symbol, MiddlewareEntry>()
|
|
32
|
-
private errorHandler: ErrorHandler | undefined
|
|
33
|
-
private notFoundHandler: NotFoundHandler | undefined
|
|
34
|
-
|
|
35
|
-
private readonly options: RouteTableOptions
|
|
36
|
-
|
|
37
|
-
constructor(options: RouteTableOptions) {
|
|
38
|
-
this.options = options
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Routes plus middleware currently registered. A reload that leaks registrations makes this grow. */
|
|
42
|
-
get size(): number {
|
|
43
|
-
return this.routes.size + this.middleware.size
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
add(method: RouteMethod, path: string, handler: Handler): () => void {
|
|
47
|
-
const pattern = compilePattern(path, this.options.trailingSlash)
|
|
48
|
-
const key = `${method} ${shapeOf(pattern)}`
|
|
49
|
-
if (this.routes.has(key)) throw new Error(`router: duplicate route ${method} ${path}`)
|
|
50
|
-
const entry: RouteEntry = { method, pattern, handler }
|
|
51
|
-
this.routes.set(key, entry)
|
|
52
|
-
return () => {
|
|
53
|
-
if (this.routes.get(key) === entry) this.routes.delete(key)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Register middleware for `path` and everything below it (`/` means every request). */
|
|
58
|
-
use(path: string, middleware: Middleware): () => void {
|
|
59
|
-
const compiled = compilePattern(path, this.options.trailingSlash)
|
|
60
|
-
const last = compiled.segments.at(-1)
|
|
61
|
-
const pattern: Pattern = last?.kind === 'wildcard'
|
|
62
|
-
? compiled
|
|
63
|
-
: { source: compiled.source, segments: [...compiled.segments, { kind: 'wildcard' }] }
|
|
64
|
-
const id = Symbol(path)
|
|
65
|
-
this.middleware.set(id, { pattern, middleware })
|
|
66
|
-
return () => { this.middleware.delete(id) }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Register the one handler that turns errors into responses. A second registration throws: the app has one voice for its errors. */
|
|
70
|
-
setErrorHandler(handler: ErrorHandler): () => void {
|
|
71
|
-
if (this.errorHandler !== undefined) throw new Error('router: an error handler is already registered')
|
|
72
|
-
this.errorHandler = handler
|
|
73
|
-
return () => { if (this.errorHandler === handler) this.errorHandler = undefined }
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Register the one handler that answers requests no route claimed. A second registration throws. */
|
|
77
|
-
setNotFoundHandler(handler: NotFoundHandler): () => void {
|
|
78
|
-
if (this.notFoundHandler !== undefined) throw new Error('router: a not-found handler is already registered')
|
|
79
|
-
this.notFoundHandler = handler
|
|
80
|
-
return () => { if (this.notFoundHandler === handler) this.notFoundHandler = undefined }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private readonly notFoundResponse = (c: RouteContext): Response | Promise<Response> =>
|
|
84
|
-
this.notFoundHandler === undefined ? new Response('Not Found', { status: 404 }) : this.notFoundHandler(c)
|
|
85
|
-
|
|
86
|
-
private async errorResponse(error: unknown, context: RouteContext): Promise<Response> {
|
|
87
|
-
if (this.errorHandler !== undefined) {
|
|
88
|
-
try {
|
|
89
|
-
return await this.errorHandler(error, context)
|
|
90
|
-
} catch (inner) {
|
|
91
|
-
this.options.onError?.(inner)
|
|
92
|
-
return new Response('Internal Server Error', { status: 500 })
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (error instanceof HttpError) return error.getResponse()
|
|
96
|
-
this.options.onError?.(error)
|
|
97
|
-
return new Response('Internal Server Error', { status: 500 })
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private hasMethod(method: RouteMethod, parts: readonly string[]): boolean {
|
|
101
|
-
for (const entry of this.routes.values()) {
|
|
102
|
-
if (entry.method === method && matchPattern(entry.pattern, parts) !== undefined) return true
|
|
103
|
-
}
|
|
104
|
-
return false
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async dispatch(
|
|
108
|
-
req: Request,
|
|
109
|
-
signal: AbortSignal,
|
|
110
|
-
connection: ConnectionInfo = {},
|
|
111
|
-
track: (task: Promise<unknown>) => void = () => undefined,
|
|
112
|
-
): Promise<Response> {
|
|
113
|
-
const parts = splitPath(new URL(req.url).pathname, this.options.trailingSlash)
|
|
114
|
-
const method = req.method.toUpperCase()
|
|
115
|
-
|
|
116
|
-
let best: { entry: RouteEntry, params: Record<string, string> } | undefined
|
|
117
|
-
const allowed = new Set<string>()
|
|
118
|
-
// HEAD is answered by the GET route when no route claims HEAD itself, and the body is dropped below.
|
|
119
|
-
const headFromGet = method === 'HEAD' && !this.hasMethod('HEAD', parts)
|
|
120
|
-
for (const entry of this.routes.values()) {
|
|
121
|
-
const params = matchPattern(entry.pattern, parts)
|
|
122
|
-
if (params === undefined) continue
|
|
123
|
-
allowed.add(entry.method)
|
|
124
|
-
if (entry.method === 'GET') allowed.add('HEAD')
|
|
125
|
-
const answers = entry.method === '*' || entry.method === method || (headFromGet && entry.method === 'GET')
|
|
126
|
-
if (!answers) continue
|
|
127
|
-
if (best === undefined || isBetter(entry, best.entry)) best = { entry, params }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const chain: Middleware[] = []
|
|
131
|
-
for (const entry of this.middleware.values()) {
|
|
132
|
-
if (matchPattern(entry.pattern, parts) !== undefined) chain.push(entry.middleware)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const final: Handler = best !== undefined
|
|
136
|
-
? best.entry.handler
|
|
137
|
-
: allowed.size > 0 && !allowed.has('*')
|
|
138
|
-
? () => new Response('Method Not Allowed', { status: 405, headers: { allow: [...allowed].sort().join(', ') } })
|
|
139
|
-
: this.notFoundResponse
|
|
140
|
-
|
|
141
|
-
const context = createContext(req, best?.params ?? {}, signal, connection, track, this.notFoundResponse)
|
|
142
|
-
try {
|
|
143
|
-
const response = await compose(chain, final)(context)
|
|
144
|
-
return method === 'HEAD' ? new Response(null, { status: response.status, statusText: response.statusText, headers: response.headers }) : response
|
|
145
|
-
} catch (error) {
|
|
146
|
-
if (signal.aborted) return new Response('Request aborted', { status: 499 })
|
|
147
|
-
return await this.errorResponse(error, context)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function isBetter(candidate: RouteEntry, current: RouteEntry): boolean {
|
|
153
|
-
const order = compareSpecificity(candidate.pattern, current.pattern)
|
|
154
|
-
if (order !== 0) return order > 0
|
|
155
|
-
return current.method === '*' && candidate.method !== '*'
|
|
156
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/** What a `Response` can be constructed from: bytes, a string, a stream, or nothing. */
|
|
2
|
-
export type ResponseBody = ConstructorParameters<typeof Response>[0]
|
|
3
|
-
|
|
4
|
-
/** HTTP methods a route can answer; `*` answers any method not claimed by a more specific route. */
|
|
5
|
-
export type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
6
|
-
export type RouteMethod = Method | '*'
|
|
7
|
-
|
|
8
|
-
/** How a trailing slash is read: `strict` keeps `/a` and `/a/` distinct, `ignore` treats them as one path. */
|
|
9
|
-
export type TrailingSlash = 'strict' | 'ignore'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Facts about the transport the request arrived on, supplied by the caller of `fetch` and never by the request itself.
|
|
13
|
-
* An in-process caller (the terminal UI) passes nothing, so a handler that needs a trusted peer must refuse it.
|
|
14
|
-
*/
|
|
15
|
-
export interface ConnectionInfo {
|
|
16
|
-
/** The peer address of the socket, when the request came over one (for example `127.0.0.1`). */
|
|
17
|
-
readonly remoteAddress?: string | undefined
|
|
18
|
-
/** The local port the socket is connected to, the real one a `Host` header should be checked against. */
|
|
19
|
-
readonly localPort?: number | undefined
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** What a handler or middleware sees. Deliberately small: the Web-standard request plus a few response helpers. */
|
|
23
|
-
export interface RouteContext {
|
|
24
|
-
/** The incoming request. */
|
|
25
|
-
readonly req: Request
|
|
26
|
-
/** Parsed `req.url`. */
|
|
27
|
-
readonly url: URL
|
|
28
|
-
/** Matched `:name` segments, and `*` for the remainder of a wildcard route. Values are URL-decoded. */
|
|
29
|
-
readonly params: Readonly<Record<string, string>>
|
|
30
|
-
/** The transport the request arrived on; empty for an in-process call. */
|
|
31
|
-
readonly connection: ConnectionInfo
|
|
32
|
-
/** Aborted when the caller aborts the request or when the router is disposed. Handlers should honor it. */
|
|
33
|
-
readonly signal: AbortSignal
|
|
34
|
-
/** Request-scoped values shared between middleware and the handler. */
|
|
35
|
-
get<T>(key: string): T | undefined
|
|
36
|
-
set(key: string, value: unknown): void
|
|
37
|
-
/**
|
|
38
|
-
* Keep this request "in flight" until `task` settles, even after the handler returned its `Response`. A handler that
|
|
39
|
-
* streams (server-sent events) registers the promise that resolves when its stream ends; disposing the router aborts
|
|
40
|
-
* `signal` and waits for it, so a stream never outlives the plugin that served it.
|
|
41
|
-
*/
|
|
42
|
-
waitUntil(task: Promise<unknown>): void
|
|
43
|
-
/**
|
|
44
|
-
* Set a header that the response helpers below (`json`, `text`, `html`, `body`, `redirect`, `notFound`) carry. A response built
|
|
45
|
-
* by hand with `new Response` is the handler's own and is left alone; middleware that must change any response reads it from `next()`.
|
|
46
|
-
*/
|
|
47
|
-
header(name: string, value: string): void
|
|
48
|
-
/** The status the response helpers use when they are not given one. */
|
|
49
|
-
status(code: number): void
|
|
50
|
-
/** One query parameter, or all of them as an object (repeated names keep the first). */
|
|
51
|
-
query(): Record<string, string>
|
|
52
|
-
query(name: string): string | undefined
|
|
53
|
-
/** One route parameter, or all of them. Same values as `params`. */
|
|
54
|
-
param(): Readonly<Record<string, string>>
|
|
55
|
-
param(name: string): string | undefined
|
|
56
|
-
json(data: unknown, status?: number): Response
|
|
57
|
-
text(body: string, status?: number): Response
|
|
58
|
-
html(body: string, status?: number): Response
|
|
59
|
-
/** A response with a raw body (bytes, a string, a stream, or nothing) and optional extra headers. */
|
|
60
|
-
body(data: ResponseBody, status?: number, headers?: Record<string, string>): Response
|
|
61
|
-
redirect(location: string, status?: 301 | 302 | 303 | 307 | 308): Response
|
|
62
|
-
/** The router's not-found response (or the registered `notFound` handler's). */
|
|
63
|
-
notFound(): Response | Promise<Response>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type Handler = (c: RouteContext) => Response | Promise<Response>
|
|
67
|
-
export type Next = () => Promise<Response>
|
|
68
|
-
export type Middleware = (c: RouteContext, next: Next) => Response | Promise<Response>
|
|
69
|
-
|
|
70
|
-
/** Turns anything a handler or middleware threw into a response. Registered with `router.onError`. */
|
|
71
|
-
export type ErrorHandler = (error: unknown, c: RouteContext) => Response | Promise<Response>
|
|
72
|
-
/** Answers a request no route claimed. Registered with `router.notFound`. */
|
|
73
|
-
export type NotFoundHandler = (c: RouteContext) => Response | Promise<Response>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/** Small response utilities shared by the middleware. Not exported from the package. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Set headers on a response a handler already built. Responses made with `new Response` are mutable; ones from `fetch` or `Response.redirect` are
|
|
5
|
-
* not, so on a failed write the response is rebuilt around the same body.
|
|
6
|
-
*/
|
|
7
|
-
export function setHeaders(response: Response, headers: Record<string, string>): Response {
|
|
8
|
-
try {
|
|
9
|
-
for (const [name, value] of Object.entries(headers)) response.headers.set(name, value)
|
|
10
|
-
return response
|
|
11
|
-
} catch {
|
|
12
|
-
const merged = new Headers(response.headers)
|
|
13
|
-
for (const [name, value] of Object.entries(headers)) merged.set(name, value)
|
|
14
|
-
return new Response(response.body, { status: response.status, statusText: response.statusText, headers: merged })
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function appendVary(response: Response, value: string): Response {
|
|
19
|
-
const current = response.headers.get('vary')
|
|
20
|
-
const present = current?.split(',').map(part => part.trim().toLowerCase()) ?? []
|
|
21
|
-
if (present.includes('*') || present.includes(value.toLowerCase())) return response
|
|
22
|
-
return setHeaders(response, { vary: current === null || current === '' ? value : `${current}, ${value}` })
|
|
23
|
-
}
|