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,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { App } from './plugins/acryl-router/src/index.ts'
|
|
3
|
+
import { bodyLimit } from './plugins/acryl-router/src/middleware/index.ts'
|
|
4
4
|
import type {} from './plugins/agent/index.ts'
|
|
5
5
|
import type {} from './plugins/extensions/index.ts'
|
|
6
6
|
import type {} from './plugins/tasks/index.ts'
|
|
@@ -10,44 +10,54 @@ export const APP_NAME = '__APP_NAME__'
|
|
|
10
10
|
/**
|
|
11
11
|
* The app's own routes on the router: `POST /ask` for the agent; `GET /extensions` and `GET /tasks` to observe what it did (every run is a
|
|
12
12
|
* durable task owning one task per tool call); `POST /tasks/cancel`. What changes is also streamed: `GET /api/state/tasks/events` and
|
|
13
|
-
* `GET /api/state/extensions/events`.
|
|
13
|
+
* `GET /api/state/extensions/events`. The mount is removed when the plugin is disposed.
|
|
14
14
|
*/
|
|
15
15
|
export const routes = {
|
|
16
16
|
name: 'routes',
|
|
17
17
|
inject: ['router', 'agent', 'extensions', 'tasks'],
|
|
18
18
|
apply(ctx: Context) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
c.json(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
28
|
-
|
|
19
|
+
// A body here is a name, an id or a question; anything bigger is refused before it is read.
|
|
20
|
+
const json = async (c: {
|
|
21
|
+
req: { json(): Promise<unknown> }
|
|
22
|
+
}): Promise<Record<string, unknown>> => {
|
|
23
|
+
const body = await c.req.json().catch(() => undefined)
|
|
24
|
+
return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
|
|
25
|
+
}
|
|
26
|
+
const app = new App()
|
|
27
|
+
.use(bodyLimit({ maxSize: 64 * 1024 }))
|
|
28
|
+
.get('/', (c) => c.json({ app: APP_NAME, ok: true }))
|
|
29
|
+
.get('/health', (c) => c.json({ ok: true }))
|
|
30
|
+
.get('/extensions', (c) =>
|
|
31
|
+
c.json({
|
|
32
|
+
installed: ctx.extensions.list(),
|
|
33
|
+
notInstalled: ctx.extensions.pending(),
|
|
34
|
+
waitingForApproval: ctx.extensions.approvals(),
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
29
37
|
// The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
|
|
30
38
|
for (const [path, decide] of [
|
|
31
39
|
['/extensions/approve', (name: string) => ctx.extensions.approve(name)],
|
|
32
40
|
['/extensions/deny', (name: string) => ctx.extensions.deny(name)],
|
|
33
41
|
] as const) {
|
|
34
|
-
|
|
35
|
-
const name =
|
|
42
|
+
app.post(path, async (c) => {
|
|
43
|
+
const { name } = await json(c)
|
|
36
44
|
if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
|
|
37
45
|
return c.json(await decide(name))
|
|
38
46
|
})
|
|
39
47
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
app
|
|
49
|
+
.get('/tasks', (c) => c.json(ctx.tasks.list().slice(0, 100)))
|
|
50
|
+
.post('/tasks/cancel', async (c) => {
|
|
51
|
+
const { id } = await json(c)
|
|
52
|
+
if (typeof id !== 'string' || !ctx.tasks.get(id))
|
|
53
|
+
return c.json({ error: 'no such task' }, 404)
|
|
54
|
+
return c.json({ cancelled: ctx.tasks.cancel(id) })
|
|
55
|
+
})
|
|
56
|
+
.post('/ask', async (c) => {
|
|
57
|
+
const { question } = await json(c)
|
|
58
|
+
if (typeof question !== 'string') return c.json({ error: 'question must be a string' }, 400)
|
|
59
|
+
return c.json(await ctx.agent.run(question))
|
|
60
|
+
})
|
|
61
|
+
ctx.router.route('/', app)
|
|
52
62
|
},
|
|
53
63
|
}
|
|
@@ -10,11 +10,12 @@ import { pinnedInstance, withPort } from '../src/plugins/instance/index.ts'
|
|
|
10
10
|
|
|
11
11
|
/** A plugin no scripted command knows: the model writes it from scratch. */
|
|
12
12
|
const COUNTER = `import type { Context } from '@deepseek-ai/cordis'
|
|
13
|
+
import { App } from 'acryl-router'
|
|
13
14
|
export const name = 'counter'
|
|
14
15
|
export const inject = ['router']
|
|
15
16
|
export function apply(ctx: Context) {
|
|
16
17
|
let count = 0
|
|
17
|
-
ctx.router.get('/count', (c) => c.json({ count: ++count }))
|
|
18
|
+
ctx.router.route('/', new App().get('/count', (c) => c.json({ count: ++count })))
|
|
18
19
|
}
|
|
19
20
|
`
|
|
20
21
|
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"lib": ["
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
5
5
|
"module": "NodeNext",
|
|
6
6
|
"moduleResolution": "NodeNext",
|
|
7
7
|
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"exactOptionalPropertyTypes": true,
|
|
10
8
|
"noImplicitOverride": true,
|
|
11
9
|
"verbatimModuleSyntax": true,
|
|
12
10
|
"erasableSyntaxOnly": true,
|
|
13
11
|
"allowImportingTsExtensions": true,
|
|
14
12
|
"noEmit": true,
|
|
15
13
|
"skipLibCheck": true,
|
|
16
|
-
"types": ["node"]
|
|
14
|
+
"types": ["node"],
|
|
15
|
+
"paths": {
|
|
16
|
+
"acryl-router": ["./src/plugins/acryl-router/src/index.ts"],
|
|
17
|
+
"acryl-router/middleware": ["./src/plugins/acryl-router/src/middleware/index.ts"],
|
|
18
|
+
"acryl-router/helpers": ["./src/plugins/acryl-router/src/helpers/index.ts"]
|
|
19
|
+
}
|
|
17
20
|
},
|
|
18
21
|
"include": ["src", "test", "extensions"]
|
|
19
22
|
}
|
|
@@ -15,7 +15,7 @@ npm run check # type-check
|
|
|
15
15
|
one off with `disabled: true`. `npm run lock` writes `blend.lock.json`: the manifest's digest and a digest of
|
|
16
16
|
every plugin folder, so a diff of the lock shows what changed.
|
|
17
17
|
- `src/app.ts` holds the registry of plugins the code provides, and mounts the manifest's rows.
|
|
18
|
-
- `src/routes.ts` is your own plugin:
|
|
18
|
+
- `src/routes.ts` is your own plugin: it builds an `App` (`import { App } from './plugins/acryl-router/src/index.ts'`) and mounts it with `ctx.router.route(prefix, app)`. Routes added to the app after it is mounted are not served. A page mounted at `/x` answers `/x`, not `/x/` (trailing slashes are strict). The router API is documented at https://acrylblends.github.io/docs/framework/the-acryl-framework and in `docs/framework` of the acrylblends repository.
|
|
19
19
|
- `src/plugins/` holds the plugins copied from the ACRYLBLENDS library. They are yours now; edit them freely.
|
|
20
20
|
Add more with `acrylblends add <plugin>`.
|
|
21
21
|
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
|
-
import
|
|
2
|
+
import { App } from './plugins/acryl-router/src/index.ts'
|
|
3
3
|
|
|
4
4
|
export const APP_NAME = '__APP_NAME__'
|
|
5
5
|
|
|
6
|
-
/** The app's own routes on the router.
|
|
6
|
+
/** The app's own routes: an `App` mounted on the router. The mount is removed when this plugin is disposed. */
|
|
7
7
|
export const routes = {
|
|
8
8
|
name: 'routes',
|
|
9
9
|
inject: ['router'],
|
|
10
10
|
apply(ctx: Context) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const app = new App()
|
|
12
|
+
.get('/', (c) => c.json({ app: APP_NAME, ok: true }))
|
|
13
|
+
.get('/health', (c) => c.json({ ok: true }))
|
|
14
|
+
ctx.router.route('/', app)
|
|
13
15
|
},
|
|
14
16
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"lib": ["
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
5
5
|
"module": "NodeNext",
|
|
6
6
|
"moduleResolution": "NodeNext",
|
|
7
7
|
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"exactOptionalPropertyTypes": true,
|
|
10
8
|
"noImplicitOverride": true,
|
|
11
9
|
"verbatimModuleSyntax": true,
|
|
12
10
|
"erasableSyntaxOnly": true,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
|
|
2
|
+
## This app (todo)
|
|
3
|
+
|
|
4
|
+
`src/todo.ts` is the whole feature: a plugin that mounts an `App` (CRUD, a page, a server-sent stream) and keeps the list in a state cell that it
|
|
5
|
+
also writes to a file in the app's home. Use it as the reference for a new plugin, and keep its tests passing when you change it.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The page: plain HTML and a few lines of script; it follows the list through the server-sent event stream. */
|
|
2
2
|
export const PAGE = `<!doctype html>
|
|
3
3
|
<meta charset="utf-8"><title>Todo</title>
|
|
4
|
-
<style>body{font:16px system-ui;max-width:32rem;margin:2rem auto}li{display:flex;gap:.5rem;padding:.25rem 0}.done span{text-decoration:line-through;opacity:.6}</style>
|
|
4
|
+
<style>body{font:16px system-ui;max-width:32rem;margin:2rem auto}ul{list-style:none;padding:0}li{display:flex;gap:.5rem;padding:.25rem 0}.done span{text-decoration:line-through;opacity:.6}</style>
|
|
5
5
|
<h1>Todo</h1>
|
|
6
6
|
<form id="add"><input id="title" placeholder="What needs doing?" autofocus> <button>Add</button></form>
|
|
7
7
|
<ul id="list"></ul>
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
7
7
|
import { dirname } from 'node:path'
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
-
import {
|
|
10
|
-
import { loopbackOnly } from './plugins/acryl-router/src/middleware/index.ts'
|
|
11
|
-
import {
|
|
9
|
+
import { App, HTTPException } from './plugins/acryl-router/src/index.ts'
|
|
10
|
+
import { bodyLimit, loopbackOnly, trimTrailingSlash } from './plugins/acryl-router/src/middleware/index.ts'
|
|
11
|
+
import { schemaValidator, streamSSE, type StandardSchema } from './plugins/acryl-router/src/helpers/index.ts'
|
|
12
12
|
import type {} from './plugins/acryl-state/src/index.ts'
|
|
13
13
|
import { PAGE } from './page.ts'
|
|
14
14
|
|
|
@@ -31,6 +31,19 @@ const NewTodo: StandardSchema<{ title: string }> = {
|
|
|
31
31
|
},
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/** Standard Schema for a change to a todo. */
|
|
35
|
+
const TodoChange: StandardSchema<{ done: boolean }> = {
|
|
36
|
+
'~standard': {
|
|
37
|
+
validate: value => {
|
|
38
|
+
const done = (value as { done?: unknown } | null)?.done
|
|
39
|
+
return typeof done === 'boolean' ? { value: { done } } : { issues: [{ message: 'done must be true or false', path: ['done'] }] }
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A todo body is small; anything bigger is refused before it is read. */
|
|
45
|
+
const MAX_BODY_BYTES = 16 * 1024
|
|
46
|
+
|
|
34
47
|
/** A missing file is a new list. Any other failure (unreadable, corrupt, half-written) refuses to start: starting empty would overwrite the user's real list on the first save. */
|
|
35
48
|
function load(file: string | undefined): Todo[] {
|
|
36
49
|
if (file === undefined) return []
|
|
@@ -65,32 +78,48 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
65
78
|
}
|
|
66
79
|
const nextId = (): number => todos.get().reduce((max, todo) => Math.max(max, todo.id), 0) + 1
|
|
67
80
|
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
api
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
81
|
+
const find = (id: number): Todo => {
|
|
82
|
+
const todo = todos.get().find(item => item.id === id)
|
|
83
|
+
if (todo === undefined) throw new HTTPException(404, { message: 'no such todo' })
|
|
84
|
+
return todo
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// `/todo/` is what people type: trimTrailingSlash answers a GET for an unknown path ending in `/` with a 301 to the path without it,
|
|
88
|
+
// so the page is found either way while each route keeps one canonical path (the router itself treats `/a` and `/a/` as different).
|
|
89
|
+
const api = new App()
|
|
90
|
+
.use(loopbackOnly({ inProcess: 'allow' }), bodyLimit({ maxSize: MAX_BODY_BYTES }), trimTrailingSlash())
|
|
91
|
+
.get('/', c => c.html(PAGE))
|
|
92
|
+
.get('/api', c => c.json(todos.get()))
|
|
93
|
+
.post('/api', schemaValidator('json', NewTodo), c => {
|
|
94
|
+
const todo: Todo = { id: nextId(), title: c.req.valid('json').title, done: false }
|
|
95
|
+
todos.update(list => [...list, todo])
|
|
96
|
+
return c.json(todo, 201)
|
|
97
|
+
})
|
|
98
|
+
.patch('/api/:id', schemaValidator('json', TodoChange), c => {
|
|
99
|
+
const { id } = find(Number(c.req.param('id')))
|
|
100
|
+
const { done } = c.req.valid('json')
|
|
101
|
+
todos.update(list => list.map(todo => todo.id === id ? { ...todo, done } : todo))
|
|
102
|
+
return c.json(find(id))
|
|
103
|
+
})
|
|
104
|
+
.delete('/api/:id', c => {
|
|
105
|
+
const { id } = find(Number(c.req.param('id')))
|
|
106
|
+
todos.update(list => list.filter(todo => todo.id !== id))
|
|
107
|
+
return c.body(null, 204)
|
|
108
|
+
})
|
|
109
|
+
.get('/events', c => streamSSE(c, async stream => {
|
|
110
|
+
const ended = new Promise<void>(resolve => { stream.onAbort(resolve) })
|
|
111
|
+
// Writes are asynchronous; chaining them keeps events in the order the list changed.
|
|
112
|
+
let queue: Promise<unknown> = Promise.resolve()
|
|
113
|
+
const send = (list: readonly Todo[]): void => { queue = queue.then(() => stream.writeSSE({ data: JSON.stringify(list) })).catch(() => {}) }
|
|
114
|
+
send(todos.get())
|
|
115
|
+
const unsubscribe = todos.subscribe(send)
|
|
116
|
+
try {
|
|
117
|
+
await ended
|
|
118
|
+
} finally {
|
|
119
|
+
unsubscribe()
|
|
120
|
+
}
|
|
121
|
+
}))
|
|
122
|
+
ctx.router.route('/todo', api)
|
|
94
123
|
|
|
95
124
|
ctx.inject(['routerHttp'], inner => { inner.routerHttp.mount('/todo') })
|
|
96
125
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
4
|
-
"lib": ["
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
5
5
|
"module": "NodeNext",
|
|
6
6
|
"moduleResolution": "NodeNext",
|
|
7
7
|
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"exactOptionalPropertyTypes": true,
|
|
10
8
|
"noImplicitOverride": true,
|
|
11
9
|
"verbatimModuleSyntax": true,
|
|
12
10
|
"erasableSyntaxOnly": true,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"examples/acryl-todo/src/index.ts": "src/todo.ts",
|
|
6
6
|
"examples/acryl-todo/src/page.ts": "src/page.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "64e7c9d2e787f74b2da82492644fbbb46a601074"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 4,
|
|
11
|
-
"digest": "sha256:
|
|
11
|
+
"digest": "sha256:b62b5167344084f7ac03c8286923f92419a4ab41c5d8235adb3247d983807aca"
|
|
12
12
|
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { Handler, Middleware, RouteContext } from './types.ts'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Run `middleware` in order around `final`. Each middleware receives `next`, which runs the rest of the chain and
|
|
5
|
-
* resolves to its response. Calling `next` twice in one middleware is a programming error and rejects.
|
|
6
|
-
*/
|
|
7
|
-
export function compose(middleware: readonly Middleware[], final: Handler): Handler {
|
|
8
|
-
return (c: RouteContext): Promise<Response> => {
|
|
9
|
-
const dispatch = async (index: number): Promise<Response> => {
|
|
10
|
-
const current = middleware[index]
|
|
11
|
-
if (current === undefined) return final(c)
|
|
12
|
-
let called = false
|
|
13
|
-
return current(c, () => {
|
|
14
|
-
if (called) return Promise.reject(new Error('router: next() called more than once'))
|
|
15
|
-
called = true
|
|
16
|
-
return dispatch(index + 1)
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
return dispatch(0)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { ConnectionInfo, ResponseBody, RouteContext } from './types.ts'
|
|
2
|
-
|
|
3
|
-
const JSON_TYPE = 'application/json; charset=utf-8'
|
|
4
|
-
const TEXT_TYPE = 'text/plain; charset=utf-8'
|
|
5
|
-
const HTML_TYPE = 'text/html; charset=utf-8'
|
|
6
|
-
|
|
7
|
-
/** Statuses that must not carry a body; a `Response` constructed with one throws. */
|
|
8
|
-
const NULL_BODY_STATUSES = new Set([101, 204, 205, 304])
|
|
9
|
-
const bodyFor = <T>(status: number, body: T): T | null => NULL_BODY_STATUSES.has(status) ? null : body
|
|
10
|
-
|
|
11
|
-
/** Build the per-request context. `notFound` is what `c.notFound()` runs: the router's own answer. */
|
|
12
|
-
export function createContext(
|
|
13
|
-
req: Request,
|
|
14
|
-
params: Record<string, string>,
|
|
15
|
-
signal: AbortSignal,
|
|
16
|
-
connection: ConnectionInfo,
|
|
17
|
-
track: (task: Promise<unknown>) => void,
|
|
18
|
-
notFound: (c: RouteContext) => Response | Promise<Response>,
|
|
19
|
-
): RouteContext {
|
|
20
|
-
const values = new Map<string, unknown>()
|
|
21
|
-
const url = new URL(req.url)
|
|
22
|
-
const defaults = new Headers()
|
|
23
|
-
let defaultStatus = 200
|
|
24
|
-
|
|
25
|
-
const respond = (body: ResponseBody, status: number | undefined, type: string | undefined, extra?: Record<string, string>): Response => {
|
|
26
|
-
const code = status ?? defaultStatus
|
|
27
|
-
const headers = new Headers(defaults)
|
|
28
|
-
if (type !== undefined && !headers.has('content-type')) headers.set('content-type', type)
|
|
29
|
-
for (const [name, value] of Object.entries(extra ?? {})) headers.set(name, value)
|
|
30
|
-
return new Response(bodyFor(code, body), { status: code, headers })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const context: RouteContext = {
|
|
34
|
-
req,
|
|
35
|
-
url,
|
|
36
|
-
params,
|
|
37
|
-
connection,
|
|
38
|
-
signal,
|
|
39
|
-
waitUntil: track,
|
|
40
|
-
get: <T>(key: string) => values.get(key) as T | undefined,
|
|
41
|
-
set: (key, value) => { values.set(key, value) },
|
|
42
|
-
header: (name, value) => { defaults.set(name, value) },
|
|
43
|
-
status: code => { defaultStatus = code },
|
|
44
|
-
query: ((name?: string) => {
|
|
45
|
-
if (name !== undefined) return url.searchParams.get(name) ?? undefined
|
|
46
|
-
const all: Record<string, string> = {}
|
|
47
|
-
for (const [key, value] of url.searchParams) if (!(key in all)) all[key] = value
|
|
48
|
-
return all
|
|
49
|
-
}) as RouteContext['query'],
|
|
50
|
-
param: ((name?: string) => name === undefined ? params : params[name]) as RouteContext['param'],
|
|
51
|
-
json: (data, status) => respond(JSON.stringify(data), status, JSON_TYPE),
|
|
52
|
-
text: (body, status) => respond(body, status, TEXT_TYPE),
|
|
53
|
-
html: (body, status) => respond(body, status, HTML_TYPE),
|
|
54
|
-
body: (data, status, headers) => respond(data, status, undefined, headers),
|
|
55
|
-
redirect: (location, status = 302) => respond(null, status, undefined, { location }),
|
|
56
|
-
notFound: () => notFound(context),
|
|
57
|
-
}
|
|
58
|
-
return context
|
|
59
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { HttpError } from '../http-error.ts'
|
|
2
|
-
import { BODY_LIMIT_KEY } from '../middleware/body-limit.ts'
|
|
3
|
-
import type { RouteContext } from '../types.ts'
|
|
4
|
-
|
|
5
|
-
export interface ReadBodyOptions {
|
|
6
|
-
/** The most bytes to accept. Defaults to the `bodyLimit` middleware's limit when it ran, otherwise 1 MiB. */
|
|
7
|
-
readonly maxBytes?: number
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const DEFAULT_MAX_BYTES = 1024 * 1024
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Read the request body as bytes, counting as it streams, and stop at the limit (a 413 `HttpError`). Stops at once when the request is aborted.
|
|
14
|
-
* A request with no body gives an empty array.
|
|
15
|
-
*/
|
|
16
|
-
export async function readBytes(c: RouteContext, options: ReadBodyOptions = {}): Promise<Uint8Array> {
|
|
17
|
-
const limit = options.maxBytes ?? c.get<number>(BODY_LIMIT_KEY) ?? DEFAULT_MAX_BYTES
|
|
18
|
-
c.signal.throwIfAborted()
|
|
19
|
-
const reader = c.req.body?.getReader()
|
|
20
|
-
if (reader === undefined) return new Uint8Array()
|
|
21
|
-
const onAbort = (): void => { void reader.cancel(c.signal.reason).catch(() => undefined) }
|
|
22
|
-
c.signal.addEventListener('abort', onAbort, { once: true })
|
|
23
|
-
const chunks: Uint8Array[] = []
|
|
24
|
-
let size = 0
|
|
25
|
-
try {
|
|
26
|
-
for (;;) {
|
|
27
|
-
const { done, value } = await reader.read()
|
|
28
|
-
if (done) break
|
|
29
|
-
size += value.byteLength
|
|
30
|
-
if (size > limit) {
|
|
31
|
-
await reader.cancel().catch(() => undefined)
|
|
32
|
-
throw new HttpError(413, { message: 'payload too large' })
|
|
33
|
-
}
|
|
34
|
-
chunks.push(value)
|
|
35
|
-
}
|
|
36
|
-
} finally {
|
|
37
|
-
c.signal.removeEventListener('abort', onAbort)
|
|
38
|
-
}
|
|
39
|
-
c.signal.throwIfAborted()
|
|
40
|
-
const bytes = new Uint8Array(size)
|
|
41
|
-
let offset = 0
|
|
42
|
-
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength }
|
|
43
|
-
return bytes
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The body as UTF-8 text, within the limit. */
|
|
47
|
-
export async function readText(c: RouteContext, options?: ReadBodyOptions): Promise<string> {
|
|
48
|
-
return new TextDecoder().decode(await readBytes(c, options))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The body parsed as JSON, within the limit. Bad JSON is a 400 `HttpError`. */
|
|
52
|
-
export async function readJson(c: RouteContext, options?: ReadBodyOptions): Promise<unknown> {
|
|
53
|
-
const text = await readText(c, options)
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(text) as unknown
|
|
56
|
-
} catch {
|
|
57
|
-
throw new HttpError(400, { message: 'invalid json' })
|
|
58
|
-
}
|
|
59
|
-
}
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { RouteContext } from '../types.ts'
|
|
2
|
-
|
|
3
|
-
export interface SseStream {
|
|
4
|
-
/** Send one event. Objects and arrays are sent as JSON; strings as they are. */
|
|
5
|
-
send(data: unknown, options?: { readonly event?: string, readonly id?: string }): void
|
|
6
|
-
/** Send a comment line (a keep-alive that clients ignore). */
|
|
7
|
-
comment(text: string): void
|
|
8
|
-
/** End the stream from the server side. */
|
|
9
|
-
close(): void
|
|
10
|
-
/** Aborted when the client goes away or the router is disposed. */
|
|
11
|
-
readonly signal: AbortSignal
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const HEADERS = {
|
|
15
|
-
'content-type': 'text/event-stream; charset=utf-8',
|
|
16
|
-
'cache-control': 'no-store',
|
|
17
|
-
'x-content-type-options': 'nosniff',
|
|
18
|
-
} as const
|
|
19
|
-
|
|
20
|
-
const encode = (text: string): Uint8Array => new TextEncoder().encode(text)
|
|
21
|
-
|
|
22
|
-
export interface SseOptions {
|
|
23
|
-
/** A comment line is sent this often so proxies and browsers keep an idle stream open. Default 15 seconds. */
|
|
24
|
-
readonly keepAliveMs?: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const DEFAULT_KEEP_ALIVE_MS = 15_000
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* A server-sent-event response. `run` starts the stream; whatever cleanup function it returns runs exactly once when the stream
|
|
31
|
-
* ends for any reason (the client disconnects, `close()`, a plugin unload, the router being disposed). The request is held in flight with `waitUntil`
|
|
32
|
-
* until then, so disposal waits for the stream instead of leaking it.
|
|
33
|
-
*/
|
|
34
|
-
export function streamSSE(c: RouteContext, run: (stream: SseStream) => void | (() => void), options: SseOptions = {}): Response {
|
|
35
|
-
let finish!: () => void
|
|
36
|
-
c.waitUntil(new Promise<void>(resolve => { finish = resolve }))
|
|
37
|
-
let controller!: ReadableStreamDefaultController<Uint8Array>
|
|
38
|
-
let cleanup: (() => void) | undefined
|
|
39
|
-
let ended = false
|
|
40
|
-
let keepAlive: ReturnType<typeof setInterval> | undefined
|
|
41
|
-
|
|
42
|
-
const end = (): void => {
|
|
43
|
-
if (ended) return
|
|
44
|
-
ended = true
|
|
45
|
-
clearInterval(keepAlive)
|
|
46
|
-
c.signal.removeEventListener('abort', end)
|
|
47
|
-
try { cleanup?.() } finally {
|
|
48
|
-
try { controller.close() } catch { /* already closed */ }
|
|
49
|
-
finish()
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const body = new ReadableStream<Uint8Array>({
|
|
54
|
-
start: ctrl => {
|
|
55
|
-
controller = ctrl
|
|
56
|
-
const stream: SseStream = {
|
|
57
|
-
signal: c.signal,
|
|
58
|
-
send: (data, options) => {
|
|
59
|
-
if (ended) return
|
|
60
|
-
const lines: string[] = []
|
|
61
|
-
if (options?.id !== undefined) lines.push(`id: ${options.id}`)
|
|
62
|
-
if (options?.event !== undefined) lines.push(`event: ${options.event}`)
|
|
63
|
-
const payload = typeof data === 'string' ? data : JSON.stringify(data)
|
|
64
|
-
for (const line of payload.split('\n')) lines.push(`data: ${line}`)
|
|
65
|
-
try { controller.enqueue(encode(`${lines.join('\n')}\n\n`)) } catch { end() }
|
|
66
|
-
},
|
|
67
|
-
comment: text => {
|
|
68
|
-
if (ended) return
|
|
69
|
-
try { controller.enqueue(encode(`: ${text.replaceAll('\n', ' ')}\n\n`)) } catch { end() }
|
|
70
|
-
},
|
|
71
|
-
close: end,
|
|
72
|
-
}
|
|
73
|
-
if (c.signal.aborted) { end(); return }
|
|
74
|
-
c.signal.addEventListener('abort', end, { once: true })
|
|
75
|
-
keepAlive = setInterval(() => { stream.comment('keepalive') }, options.keepAliveMs ?? DEFAULT_KEEP_ALIVE_MS)
|
|
76
|
-
keepAlive.unref()
|
|
77
|
-
cleanup = run(stream) ?? undefined
|
|
78
|
-
if (ended) { try { cleanup?.() } catch { /* the stream already ended */ } }
|
|
79
|
-
},
|
|
80
|
-
cancel: end,
|
|
81
|
-
})
|
|
82
|
-
return new Response(body, { headers: HEADERS })
|
|
83
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { HttpError } from '../http-error.ts'
|
|
2
|
-
import type { Middleware, RouteContext } from '../types.ts'
|
|
3
|
-
import { readJson } from './read-body.ts'
|
|
4
|
-
|
|
5
|
-
/** The slice of the Standard Schema interface this needs; Schemastery (what Cordis uses), Zod and Valibot all implement it. */
|
|
6
|
-
export interface StandardSchema<T = unknown> {
|
|
7
|
-
readonly '~standard': {
|
|
8
|
-
validate(value: unknown): StandardResult<T> | Promise<StandardResult<T>>
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
type StandardResult<T> =
|
|
13
|
-
| { readonly value: T, readonly issues?: undefined }
|
|
14
|
-
| { readonly issues: readonly { readonly message: string, readonly path?: readonly unknown[] | undefined }[] }
|
|
15
|
-
|
|
16
|
-
export type ValidationTarget = 'json' | 'query' | 'param'
|
|
17
|
-
|
|
18
|
-
const key = (target: ValidationTarget): string => `valid:${target}`
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Validate part of the request against a schema before the handler runs. A failure is a 400 with the issues; success stores the
|
|
22
|
-
* parsed value, which the handler reads with `valid(c, target)`. `json` reads the body within the limit; `query` is the first value of each query
|
|
23
|
-
* parameter; `param` is the matched path parameters.
|
|
24
|
-
*/
|
|
25
|
-
export function validate<T>(target: ValidationTarget, schema: StandardSchema<T>): Middleware {
|
|
26
|
-
return async (c, next) => {
|
|
27
|
-
const input = target === 'json' ? await readJson(c) : target === 'query' ? c.query() : { ...c.param() }
|
|
28
|
-
const result = await schema['~standard'].validate(input)
|
|
29
|
-
if (result.issues !== undefined) {
|
|
30
|
-
throw new HttpError(400, {
|
|
31
|
-
response: c.json({ error: `invalid ${target}`, issues: result.issues.map(issue => ({ message: issue.message, path: (issue.path ?? []).map(part => String(typeof part === 'object' && part !== null && 'key' in part ? (part as { key: unknown }).key : part)).join('.') })) }, 400),
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
c.set(key(target), result.value)
|
|
35
|
-
return next()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** The value `validate(target, schema)` stored. Throws if that middleware did not run on this route. */
|
|
40
|
-
export function valid<T>(c: RouteContext, target: ValidationTarget): T {
|
|
41
|
-
const value = c.get<T>(key(target))
|
|
42
|
-
if (value === undefined) throw new Error(`valid("${target}"): no validate("${target}", schema) ran before this handler`)
|
|
43
|
-
return value
|
|
44
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An error that carries the response it should become. A handler or middleware throws it to stop a request
|
|
3
|
-
* with a status: `throw new HttpError(401, { message: 'Sign in first' })`. The router answers with `getResponse()`; a registered
|
|
4
|
-
* `onError` handler still sees every other error.
|
|
5
|
-
*/
|
|
6
|
-
export class HttpError extends Error {
|
|
7
|
-
readonly status: number
|
|
8
|
-
private readonly response: Response | undefined
|
|
9
|
-
|
|
10
|
-
constructor(status: number, options: { readonly message?: string, readonly response?: Response, readonly cause?: unknown } = {}) {
|
|
11
|
-
super(options.message ?? `HTTP ${String(status)}`, options.cause === undefined ? undefined : { cause: options.cause })
|
|
12
|
-
this.name = 'HttpError'
|
|
13
|
-
this.status = status
|
|
14
|
-
this.response = options.response
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** The response this error stands for: the one it was given, or its message as plain text with its status. */
|
|
18
|
-
getResponse(): Response {
|
|
19
|
-
return this.response ?? new Response(this.message, { status: this.status, headers: { 'content-type': 'text/plain; charset=utf-8' } })
|
|
20
|
-
}
|
|
21
|
-
}
|