acrylblends 0.2.0 → 0.4.0
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 +22 -16
- package/dist/index.js +65 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugins/acryl-inspect/LICENSE +21 -0
- package/plugins/acryl-inspect/plugin.json +12 -0
- package/plugins/acryl-inspect/src/index.ts +3 -0
- package/plugins/acryl-inspect/src/report.ts +126 -0
- package/plugins/acryl-inspect/src/service.ts +68 -0
- package/plugins/acryl-inspect/tests/report.spec.ts +63 -0
- package/plugins/acryl-inspect/tests/service.spec.ts +121 -0
- package/plugins/acryl-inspect/upstream.json +18 -0
- package/plugins/acryl-node-server/LICENSE +47 -0
- package/plugins/acryl-node-server/plugin.json +13 -0
- 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 +107 -0
- 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 +141 -0
- package/plugins/acryl-node-server/upstream.json +18 -0
- package/plugins/acryl-router/LICENSE +47 -0
- package/plugins/acryl-router/plugin.json +13 -0
- 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 -0
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -0
- package/plugins/acryl-router/src/middleware/index.ts +25 -0
- package/plugins/acryl-router/src/middleware/loopback-only.ts +88 -0
- package/plugins/acryl-router/src/service.ts +356 -0
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +64 -0
- package/plugins/acryl-router/tests/loader.spec.ts +72 -0
- package/plugins/acryl-router/tests/service.spec.ts +409 -0
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +20 -0
- package/plugins/acryl-router-http/LICENSE +21 -0
- package/plugins/acryl-router-http/plugin.json +12 -0
- package/plugins/acryl-router-http/src/bridge.ts +54 -0
- package/plugins/acryl-router-http/src/index.ts +63 -0
- package/plugins/acryl-router-http/src/serve.ts +24 -0
- package/plugins/acryl-router-http/tests/adapter.spec.ts +174 -0
- package/plugins/acryl-router-http/tests/stream.spec.ts +133 -0
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +101 -0
- package/plugins/acryl-router-http/upstream.json +22 -0
- package/plugins/acryl-state/LICENSE +21 -0
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +12 -0
- package/plugins/acryl-state/src/cell.ts +67 -0
- package/plugins/acryl-state/src/index.ts +3 -0
- package/plugins/acryl-state/src/react.ts +11 -0
- package/plugins/acryl-state/src/service.ts +119 -0
- package/plugins/acryl-state/tests/cell.spec.ts +71 -0
- package/plugins/acryl-state/tests/react.spec.ts +39 -0
- package/plugins/acryl-state/tests/service.spec.ts +132 -0
- package/plugins/acryl-state/upstream.json +22 -0
- package/plugins/builder/script.ts +6 -10
- package/plugins/extensions/index.ts +20 -3
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +12 -6
- package/plugins/tasks/index.ts +14 -0
- package/plugins/tsconfig.json +14 -5
- package/templates/agent/blend.yaml +5 -7
- package/templates/agent/src/app.ts +8 -8
- package/templates/agent/src/routes.ts +39 -38
- package/templates/agent/template.json +3 -3
- package/templates/agent/test/builder.test.ts +22 -14
- package/templates/agent/test/model.test.ts +3 -3
- package/templates/agent/tsconfig.json +8 -5
- package/templates/{app → blank}/blend.yaml +3 -3
- package/templates/{app → blank}/src/app.ts +5 -5
- package/templates/blank/src/routes.ts +16 -0
- package/templates/blank/template.json +1 -0
- package/templates/{app → blank}/test/app.test.ts +1 -1
- package/templates/{app → blank}/tsconfig.json +2 -4
- package/templates/todo/README.md +27 -0
- package/templates/todo/blend.yaml +25 -0
- package/templates/todo/gitignore +2 -0
- package/templates/todo/package.json +23 -0
- package/templates/todo/src/app.ts +71 -0
- package/templates/todo/src/main.ts +19 -0
- package/templates/todo/src/page.ts +28 -0
- package/templates/todo/src/todo.ts +123 -0
- package/templates/todo/template.json +1 -0
- package/templates/todo/test/todo.test.ts +145 -0
- package/templates/todo/tsconfig.json +17 -0
- package/templates/todo/upstream.json +12 -0
- package/plugins/events/index.ts +0 -42
- package/plugins/events/plugin.json +0 -9
- package/plugins/http/index.ts +0 -64
- package/plugins/http/plugin.json +0 -10
- package/plugins/server/index.ts +0 -138
- package/plugins/server/plugin.json +0 -10
- package/templates/app/src/routes.ts +0 -14
- package/templates/app/template.json +0 -1
- /package/templates/{app → blank}/README.md +0 -0
- /package/templates/{app → blank}/gitignore +0 -0
- /package/templates/{app → blank}/package.json +0 -0
- /package/templates/{app → blank}/src/main.ts +0 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Bearer Auth Middleware for App.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from '../../context.ts'
|
|
7
|
+
import { HTTPException } from '../../http-exception.ts'
|
|
8
|
+
import type { Env, MiddlewareHandler } from '../../types.ts'
|
|
9
|
+
import { timingSafeEqual } from '../../utils/buffer.ts'
|
|
10
|
+
import type { ContentfulStatusCode } from '../../utils/http-status.ts'
|
|
11
|
+
|
|
12
|
+
const TOKEN_STRINGS = '[A-Za-z0-9._~+/-]+=*'
|
|
13
|
+
const PREFIX = 'Bearer'
|
|
14
|
+
const HEADER = 'Authorization'
|
|
15
|
+
|
|
16
|
+
type MessageFunction = (c: Context) => string | object | Promise<string | object>
|
|
17
|
+
type CustomizedErrorResponseOptions = {
|
|
18
|
+
wwwAuthenticateHeader?: string | object | MessageFunction
|
|
19
|
+
message?: string | object | MessageFunction
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type BearerAuthOptions<E extends Env = Env> =
|
|
23
|
+
| {
|
|
24
|
+
token: string | string[]
|
|
25
|
+
realm?: string
|
|
26
|
+
prefix?: string
|
|
27
|
+
headerName?: string
|
|
28
|
+
hashFunction?: Function
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated Use noAuthenticationHeader.message instead
|
|
31
|
+
*/
|
|
32
|
+
noAuthenticationHeaderMessage?: string | object | MessageFunction
|
|
33
|
+
noAuthenticationHeader?: CustomizedErrorResponseOptions
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated Use invalidAuthenticationHeader.message instead
|
|
36
|
+
*/
|
|
37
|
+
invalidAuthenticationHeaderMessage?: string | object | MessageFunction
|
|
38
|
+
invalidAuthenticationHeader?: CustomizedErrorResponseOptions
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated Use invalidToken.message instead
|
|
41
|
+
*/
|
|
42
|
+
invalidTokenMessage?: string | object | MessageFunction
|
|
43
|
+
invalidToken?: CustomizedErrorResponseOptions
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
realm?: string
|
|
47
|
+
prefix?: string
|
|
48
|
+
headerName?: string
|
|
49
|
+
verifyToken: (token: string, c: Context<E>) => boolean | Promise<boolean>
|
|
50
|
+
hashFunction?: Function
|
|
51
|
+
/**
|
|
52
|
+
* @deprecated Use noAuthenticationHeader.message instead
|
|
53
|
+
*/
|
|
54
|
+
noAuthenticationHeaderMessage?: string | object | MessageFunction
|
|
55
|
+
noAuthenticationHeader?: CustomizedErrorResponseOptions
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated Use invalidAuthenticationHeader.message instead
|
|
58
|
+
*/
|
|
59
|
+
invalidAuthenticationHeaderMessage?: string | object | MessageFunction
|
|
60
|
+
invalidAuthenticationHeader?: CustomizedErrorResponseOptions
|
|
61
|
+
/**
|
|
62
|
+
* @deprecated Use invalidToken.message instead
|
|
63
|
+
*/
|
|
64
|
+
invalidTokenMessage?: string | object | MessageFunction
|
|
65
|
+
invalidToken?: CustomizedErrorResponseOptions
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Bearer Auth Middleware for App.
|
|
70
|
+
*
|
|
71
|
+
*
|
|
72
|
+
* @template E - The environment type.
|
|
73
|
+
* @param {BearerAuthOptions} options - The options for the bearer authentication middleware.
|
|
74
|
+
* @param {string | string[]} [options.token] - The string or array of strings to validate the incoming bearer token against.
|
|
75
|
+
* @param {Function} [options.verifyToken] - The function to verify the token.
|
|
76
|
+
* @param {string} [options.realm=""] - The domain name of the realm, as part of the returned WWW-Authenticate challenge header.
|
|
77
|
+
* @param {string} [options.prefix="Bearer"] - The prefix (or known as `schema`) for the Authorization header value. If set to the empty string, no prefix is expected.
|
|
78
|
+
* @param {string} [options.headerName=Authorization] - The header name.
|
|
79
|
+
* @param {Function} [options.hashFunction] - A function to handle hashing for safe comparison of authentication tokens.
|
|
80
|
+
* @param {string | object | MessageFunction} [options.noAuthenticationHeader.message="Unauthorized"] - The no authentication header message.
|
|
81
|
+
* @param {string | object | MessageFunction} [options.noAuthenticationHeader.wwwAuthenticateHeader="Bearer realm=\"\""] - The response header value for the WWW-Authenticate header when no authentication header is provided.
|
|
82
|
+
* @param {string | object | MessageFunction} [options.invalidAuthenticationHeader.message="Bad Request"] - The invalid authentication header message.
|
|
83
|
+
* @param {string | object | MessageFunction} [options.invalidAuthenticationHeader.wwwAuthenticateHeader="Bearer error=\"invalid_request\""] - The response header value for the WWW-Authenticate header when authentication header is invalid.
|
|
84
|
+
* @param {string | object | MessageFunction} [options.invalidToken.message="Unauthorized"] - The invalid token message.
|
|
85
|
+
* @param {string | object | MessageFunction} [options.invalidToken.wwwAuthenticateHeader="Bearer error=\"invalid_token\""] - The response header value for the WWW-Authenticate header when token is invalid.
|
|
86
|
+
* @returns {MiddlewareHandler<E>} The middleware handler function.
|
|
87
|
+
* @throws {Error} If neither "token" nor "verifyToken" options are provided.
|
|
88
|
+
* @throws {HTTPException} If authentication fails, with 401 status code for missing or invalid token, or 400 status code for invalid request.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const app = new App()
|
|
93
|
+
*
|
|
94
|
+
* const token = process.env.API_TOKEN // never a literal in source
|
|
95
|
+
*
|
|
96
|
+
* app.use('/api/*', bearerAuth({ token }))
|
|
97
|
+
*
|
|
98
|
+
* app.get('/api/page', (c) => {
|
|
99
|
+
* return c.json({ message: 'You are authorized' })
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export const bearerAuth = <E extends Env = Env>(
|
|
104
|
+
options: BearerAuthOptions<E>
|
|
105
|
+
): MiddlewareHandler<E> => {
|
|
106
|
+
if (!('token' in options || 'verifyToken' in options)) {
|
|
107
|
+
throw new Error('bearer auth middleware requires options for "token" or "verifyToken"')
|
|
108
|
+
}
|
|
109
|
+
if (!options.realm) {
|
|
110
|
+
options.realm = ''
|
|
111
|
+
}
|
|
112
|
+
if (options.prefix === undefined) {
|
|
113
|
+
options.prefix = PREFIX
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const realm = options.realm?.replace(/"/g, '\\"')
|
|
117
|
+
const prefix = options.prefix
|
|
118
|
+
const tokenRegexp = new RegExp(`^${TOKEN_STRINGS}$`)
|
|
119
|
+
const wwwAuthenticatePrefix = prefix === '' ? '' : `${prefix} `
|
|
120
|
+
|
|
121
|
+
const throwHTTPException = async (
|
|
122
|
+
c: Context,
|
|
123
|
+
status: ContentfulStatusCode,
|
|
124
|
+
wwwAuthenticateHeader: string | object | MessageFunction,
|
|
125
|
+
messageOption: string | object | MessageFunction
|
|
126
|
+
): Promise<Response> => {
|
|
127
|
+
const wwwAuthenticateHeaderValue: string | object =
|
|
128
|
+
typeof wwwAuthenticateHeader === 'function'
|
|
129
|
+
? await wwwAuthenticateHeader(c)
|
|
130
|
+
: wwwAuthenticateHeader
|
|
131
|
+
|
|
132
|
+
const headers = {
|
|
133
|
+
'WWW-Authenticate':
|
|
134
|
+
typeof wwwAuthenticateHeaderValue === 'string'
|
|
135
|
+
? wwwAuthenticateHeaderValue
|
|
136
|
+
: `${wwwAuthenticatePrefix}${Object.entries(wwwAuthenticateHeaderValue)
|
|
137
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
138
|
+
.join(',')}`,
|
|
139
|
+
}
|
|
140
|
+
const responseMessage =
|
|
141
|
+
typeof messageOption === 'function' ? await messageOption(c) : messageOption
|
|
142
|
+
const res =
|
|
143
|
+
typeof responseMessage === 'string'
|
|
144
|
+
? new Response(responseMessage, { status, headers })
|
|
145
|
+
: new Response(JSON.stringify(responseMessage), {
|
|
146
|
+
status,
|
|
147
|
+
headers: {
|
|
148
|
+
...headers,
|
|
149
|
+
'content-type': 'application/json',
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
throw new HTTPException(status, { res })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return async function bearerAuth(c, next) {
|
|
156
|
+
const headerToken = c.req.header(options.headerName || HEADER)
|
|
157
|
+
if (!headerToken) {
|
|
158
|
+
// No Authorization header
|
|
159
|
+
await throwHTTPException(
|
|
160
|
+
c,
|
|
161
|
+
401,
|
|
162
|
+
options.noAuthenticationHeader?.wwwAuthenticateHeader ||
|
|
163
|
+
`${wwwAuthenticatePrefix}realm="${realm}"`,
|
|
164
|
+
options.noAuthenticationHeader?.message ||
|
|
165
|
+
options.noAuthenticationHeaderMessage ||
|
|
166
|
+
'Unauthorized'
|
|
167
|
+
)
|
|
168
|
+
} else {
|
|
169
|
+
let tokenValue: string | undefined
|
|
170
|
+
|
|
171
|
+
if (prefix === '') {
|
|
172
|
+
tokenValue = headerToken
|
|
173
|
+
} else {
|
|
174
|
+
const headerLower = headerToken.toLowerCase()
|
|
175
|
+
const prefixLower = prefix.toLowerCase()
|
|
176
|
+
if (headerLower.startsWith(prefixLower) && headerToken[prefix.length] === ' ') {
|
|
177
|
+
tokenValue = headerToken.slice(prefix.length).trimStart()
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!tokenValue || !tokenRegexp.test(tokenValue)) {
|
|
182
|
+
// Invalid Request
|
|
183
|
+
await throwHTTPException(
|
|
184
|
+
c,
|
|
185
|
+
400,
|
|
186
|
+
options.invalidAuthenticationHeader?.wwwAuthenticateHeader ||
|
|
187
|
+
`${wwwAuthenticatePrefix}error="invalid_request"`,
|
|
188
|
+
options.invalidAuthenticationHeader?.message ||
|
|
189
|
+
options.invalidAuthenticationHeaderMessage ||
|
|
190
|
+
'Bad Request'
|
|
191
|
+
)
|
|
192
|
+
} else {
|
|
193
|
+
let equal = false
|
|
194
|
+
if ('verifyToken' in options) {
|
|
195
|
+
equal = await options.verifyToken(tokenValue, c)
|
|
196
|
+
} else if (typeof options.token === 'string') {
|
|
197
|
+
equal = await timingSafeEqual(options.token, tokenValue, options.hashFunction)
|
|
198
|
+
} else if (Array.isArray(options.token) && options.token.length > 0) {
|
|
199
|
+
for (const token of options.token) {
|
|
200
|
+
if (await timingSafeEqual(token, tokenValue, options.hashFunction)) {
|
|
201
|
+
equal = true
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!equal) {
|
|
207
|
+
// Invalid Token
|
|
208
|
+
await throwHTTPException(
|
|
209
|
+
c,
|
|
210
|
+
401,
|
|
211
|
+
options.invalidToken?.wwwAuthenticateHeader ||
|
|
212
|
+
`${wwwAuthenticatePrefix}error="invalid_token"`,
|
|
213
|
+
options.invalidToken?.message || options.invalidTokenMessage || 'Unauthorized'
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
await next()
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { bodyLimit } from './index.ts'
|
|
2
|
+
import { App } from '../../app.ts'
|
|
3
|
+
|
|
4
|
+
const buildRequestInit = (init: RequestInit = {}): RequestInit & { duplex: 'half' } => {
|
|
5
|
+
const headers: Record<string, string> = {
|
|
6
|
+
'Content-Type': 'text/plain',
|
|
7
|
+
}
|
|
8
|
+
if (typeof init.body === 'string') {
|
|
9
|
+
headers['Content-Length'] = init.body.length.toString()
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers,
|
|
14
|
+
body: null,
|
|
15
|
+
...init,
|
|
16
|
+
duplex: 'half',
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Body Limit Middleware', () => {
|
|
21
|
+
let app: App
|
|
22
|
+
|
|
23
|
+
const exampleText = 'data is so hot' // 14byte
|
|
24
|
+
const exampleText2 = 'data is so hot and cute' // 23byte
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
app = new App()
|
|
28
|
+
app.use('*', bodyLimit({ maxSize: 14 }))
|
|
29
|
+
app.get('/', (c) => c.text('index'))
|
|
30
|
+
app.post('/body-limit-15byte', async (c) => {
|
|
31
|
+
return c.text(await c.req.raw.text())
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('GET request', () => {
|
|
36
|
+
it('should return 200 response', async () => {
|
|
37
|
+
const res = await app.request('/')
|
|
38
|
+
expect(res).not.toBeNull()
|
|
39
|
+
expect(res.status).toBe(200)
|
|
40
|
+
expect(await res.text()).toBe('index')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('POST request', () => {
|
|
45
|
+
describe('string body', () => {
|
|
46
|
+
it('should return 200 response', async () => {
|
|
47
|
+
const res = await app.request('/body-limit-15byte', buildRequestInit({ body: exampleText }))
|
|
48
|
+
|
|
49
|
+
expect(res).not.toBeNull()
|
|
50
|
+
expect(res.status).toBe(200)
|
|
51
|
+
expect(await res.text()).toBe(exampleText)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should return 413 response', async () => {
|
|
55
|
+
const res = await app.request(
|
|
56
|
+
'/body-limit-15byte',
|
|
57
|
+
buildRequestInit({ body: exampleText2 })
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
expect(res).not.toBeNull()
|
|
61
|
+
expect(res.status).toBe(413)
|
|
62
|
+
expect(await res.text()).toBe('Payload Too Large')
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('ReadableStream body', () => {
|
|
67
|
+
it('should return 200 response', async () => {
|
|
68
|
+
const contents = ['a', 'b', 'c']
|
|
69
|
+
const stream = new ReadableStream({
|
|
70
|
+
start(controller) {
|
|
71
|
+
while (contents.length) {
|
|
72
|
+
controller.enqueue(new TextEncoder().encode(contents.shift() as string))
|
|
73
|
+
}
|
|
74
|
+
controller.close()
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
const res = await app.request('/body-limit-15byte', buildRequestInit({ body: stream }))
|
|
78
|
+
|
|
79
|
+
expect(res).not.toBeNull()
|
|
80
|
+
expect(res.status).toBe(200)
|
|
81
|
+
expect(await res.text()).toBe('abc')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should return 413 response', async () => {
|
|
85
|
+
const readSpy = vi.fn().mockImplementation(() => {
|
|
86
|
+
return {
|
|
87
|
+
done: false,
|
|
88
|
+
value: new TextEncoder().encode(exampleText),
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
const stream = new ReadableStream()
|
|
92
|
+
vi.spyOn(stream, 'getReader').mockReturnValue({
|
|
93
|
+
read: readSpy,
|
|
94
|
+
} as unknown as ReadableStreamDefaultReader)
|
|
95
|
+
const res = await app.request('/body-limit-15byte', buildRequestInit({ body: stream }))
|
|
96
|
+
|
|
97
|
+
expect(res).not.toBeNull()
|
|
98
|
+
expect(res.status).toBe(413)
|
|
99
|
+
expect(readSpy).toHaveBeenCalledTimes(2)
|
|
100
|
+
expect(await res.text()).toBe('Payload Too Large')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('custom error handler', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
app = new App()
|
|
108
|
+
app.post(
|
|
109
|
+
'/text-limit-15byte-custom',
|
|
110
|
+
bodyLimit({
|
|
111
|
+
maxSize: 15,
|
|
112
|
+
onError: (c) => {
|
|
113
|
+
return c.text('no', 413)
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
(c) => {
|
|
117
|
+
return c.text('yes')
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should return the custom error handler', async () => {
|
|
123
|
+
const res = await app.request(
|
|
124
|
+
'/text-limit-15byte-custom',
|
|
125
|
+
buildRequestInit({ body: exampleText2 })
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
expect(res).not.toBeNull()
|
|
129
|
+
expect(res.status).toBe(413)
|
|
130
|
+
expect(await res.text()).toBe('no')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('Transfer-Encoding and Content-Length headers', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
app = new App()
|
|
137
|
+
app.use('*', bodyLimit({ maxSize: 10 }))
|
|
138
|
+
app.post('/test', async (c) => {
|
|
139
|
+
return c.text(await c.req.text())
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should prioritize Transfer-Encoding over Content-Length', async () => {
|
|
144
|
+
// Create a chunked body that exceeds the limit
|
|
145
|
+
const largeContent = 'this is a large content that exceeds 10 bytes'
|
|
146
|
+
const chunks = [largeContent.slice(0, 20), largeContent.slice(20)]
|
|
147
|
+
|
|
148
|
+
const stream = new ReadableStream({
|
|
149
|
+
start(controller) {
|
|
150
|
+
chunks.forEach((chunk) => {
|
|
151
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
152
|
+
})
|
|
153
|
+
controller.close()
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const res = await app.request('/test', {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Length': '5', // Small content-length (bypass attempt)
|
|
161
|
+
'Transfer-Encoding': 'chunked', // But chunked encoding with large content
|
|
162
|
+
},
|
|
163
|
+
body: stream,
|
|
164
|
+
duplex: 'half',
|
|
165
|
+
} as RequestInit)
|
|
166
|
+
|
|
167
|
+
// Should reject based on actual chunked content size, not Content-Length
|
|
168
|
+
expect(res.status).toBe(413)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should handle only Content-Length header correctly', async () => {
|
|
172
|
+
const smallContent = 'small'
|
|
173
|
+
const res = await app.request('/test', buildRequestInit({ body: smallContent }))
|
|
174
|
+
|
|
175
|
+
expect(res.status).toBe(200)
|
|
176
|
+
expect(await res.text()).toBe(smallContent)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('should handle only Transfer-Encoding header correctly', async () => {
|
|
180
|
+
const content = 'test'
|
|
181
|
+
const stream = new ReadableStream({
|
|
182
|
+
start(controller) {
|
|
183
|
+
controller.enqueue(new TextEncoder().encode(content))
|
|
184
|
+
controller.close()
|
|
185
|
+
},
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const res = await app.request('/test', {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: {
|
|
191
|
+
'Transfer-Encoding': 'chunked',
|
|
192
|
+
},
|
|
193
|
+
body: stream,
|
|
194
|
+
duplex: 'half',
|
|
195
|
+
} as RequestInit)
|
|
196
|
+
|
|
197
|
+
expect(res.status).toBe(200)
|
|
198
|
+
expect(await res.text()).toBe(content)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
describe('chunked body bypass scenarios', () => {
|
|
203
|
+
const handler = vi.fn()
|
|
204
|
+
|
|
205
|
+
const buildOversizedRequestInit = (): RequestInit & { duplex: 'half' } => ({
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Transfer-Encoding': 'chunked' },
|
|
208
|
+
body: new ReadableStream({
|
|
209
|
+
start(controller) {
|
|
210
|
+
controller.enqueue(new TextEncoder().encode('1234'))
|
|
211
|
+
controller.enqueue(new TextEncoder().encode('5678901234567890'))
|
|
212
|
+
controller.close()
|
|
213
|
+
},
|
|
214
|
+
}),
|
|
215
|
+
duplex: 'half',
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
beforeEach(() => {
|
|
219
|
+
handler.mockReset()
|
|
220
|
+
app = new App()
|
|
221
|
+
app.use('*', bodyLimit({ maxSize: 8 }))
|
|
222
|
+
app.post('/no-read', (c) => {
|
|
223
|
+
handler()
|
|
224
|
+
return c.text('NO-READ-OK')
|
|
225
|
+
})
|
|
226
|
+
app.post('/partial-read', async (c) => {
|
|
227
|
+
handler()
|
|
228
|
+
const reader = c.req.raw.body?.getReader()
|
|
229
|
+
const chunk = reader ? await reader.read() : { value: undefined }
|
|
230
|
+
return c.text(new TextDecoder().decode(chunk.value))
|
|
231
|
+
})
|
|
232
|
+
app.post('/swallow-error', async (c) => {
|
|
233
|
+
handler()
|
|
234
|
+
const body = c.req.raw.body
|
|
235
|
+
if (!body) {
|
|
236
|
+
return c.text('no body')
|
|
237
|
+
}
|
|
238
|
+
const reader = body.getReader()
|
|
239
|
+
let seen = ''
|
|
240
|
+
try {
|
|
241
|
+
for (;;) {
|
|
242
|
+
const { done, value } = await reader.read()
|
|
243
|
+
if (done) {
|
|
244
|
+
break
|
|
245
|
+
}
|
|
246
|
+
seen += new TextDecoder().decode(value)
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// intentionally swallow read errors
|
|
250
|
+
}
|
|
251
|
+
return c.text(`processed:${seen}`)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('should reject when handler does not read the body', async () => {
|
|
256
|
+
const res = await app.request('/no-read', buildOversizedRequestInit())
|
|
257
|
+
expect(res.status).toBe(413)
|
|
258
|
+
expect(handler).not.toHaveBeenCalled()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should reject when handler reads only the first chunk', async () => {
|
|
262
|
+
const res = await app.request('/partial-read', buildOversizedRequestInit())
|
|
263
|
+
expect(res.status).toBe(413)
|
|
264
|
+
expect(handler).not.toHaveBeenCalled()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('should reject when handler swallows body read errors', async () => {
|
|
268
|
+
const res = await app.request('/swallow-error', buildOversizedRequestInit())
|
|
269
|
+
expect(res.status).toBe(413)
|
|
270
|
+
expect(handler).not.toHaveBeenCalled()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Body Limit Middleware for App.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context } from '../../context.ts'
|
|
7
|
+
import { HTTPException } from '../../http-exception.ts'
|
|
8
|
+
import type { MiddlewareHandler } from '../../types.ts'
|
|
9
|
+
|
|
10
|
+
const ERROR_MESSAGE = 'Payload Too Large'
|
|
11
|
+
|
|
12
|
+
type OnError = (c: Context) => Response | Promise<Response>
|
|
13
|
+
type BodyLimitOptions = {
|
|
14
|
+
maxSize: number
|
|
15
|
+
onError?: OnError
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Body Limit Middleware for App.
|
|
20
|
+
*
|
|
21
|
+
*
|
|
22
|
+
* @param {BodyLimitOptions} options - The options for the body limit middleware.
|
|
23
|
+
* @param {number} options.maxSize - The maximum body size allowed.
|
|
24
|
+
* @param {OnError} [options.onError] - The error handler to be invoked if the specified body size is exceeded.
|
|
25
|
+
* @returns {MiddlewareHandler} The middleware handler function.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const app = new App()
|
|
30
|
+
*
|
|
31
|
+
* app.post(
|
|
32
|
+
* '/upload',
|
|
33
|
+
* bodyLimit({
|
|
34
|
+
* maxSize: 50 * 1024, // 50kb
|
|
35
|
+
* onError: (c) => {
|
|
36
|
+
* return c.text('overflow :(', 413)
|
|
37
|
+
* },
|
|
38
|
+
* }),
|
|
39
|
+
* async (c) => {
|
|
40
|
+
* const body = await c.req.parseBody()
|
|
41
|
+
* if (body['file'] instanceof File) {
|
|
42
|
+
* console.log(`Got file sized: ${body['file'].size}`)
|
|
43
|
+
* }
|
|
44
|
+
* return c.text('pass :)')
|
|
45
|
+
* }
|
|
46
|
+
* )
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export const bodyLimit = (options: BodyLimitOptions): MiddlewareHandler => {
|
|
50
|
+
const onError: OnError =
|
|
51
|
+
options.onError ||
|
|
52
|
+
(() => {
|
|
53
|
+
const res = new Response(ERROR_MESSAGE, {
|
|
54
|
+
status: 413,
|
|
55
|
+
})
|
|
56
|
+
throw new HTTPException(413, { res })
|
|
57
|
+
})
|
|
58
|
+
const maxSize = options.maxSize
|
|
59
|
+
|
|
60
|
+
return async function bodyLimit(c, next) {
|
|
61
|
+
if (!c.req.raw.body) {
|
|
62
|
+
// maybe GET or HEAD request
|
|
63
|
+
return next()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hasTransferEncoding = c.req.raw.headers.has('transfer-encoding')
|
|
67
|
+
const hasContentLength = c.req.raw.headers.has('content-length')
|
|
68
|
+
|
|
69
|
+
if (hasContentLength && !hasTransferEncoding) {
|
|
70
|
+
// Only Content-Length present - we can trust it
|
|
71
|
+
const contentLength = parseInt(c.req.raw.headers.get('content-length') || '0', 10)
|
|
72
|
+
return contentLength > maxSize ? onError(c) : next()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Transfer-Encoding present (chunked) or no length headers.
|
|
76
|
+
// Per RFC 7230, when both are present Transfer-Encoding takes precedence
|
|
77
|
+
// and Content-Length is ignored. Read the body up-front so the size check
|
|
78
|
+
// is final before the handler runs, regardless of how (or whether) the
|
|
79
|
+
// handler reads the body.
|
|
80
|
+
let size = 0
|
|
81
|
+
const chunks: Uint8Array[] = []
|
|
82
|
+
const rawReader = c.req.raw.body.getReader()
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { done, value } = await rawReader.read()
|
|
85
|
+
if (done) {
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
size += value.length
|
|
89
|
+
if (size > maxSize) {
|
|
90
|
+
return onError(c)
|
|
91
|
+
}
|
|
92
|
+
chunks.push(value)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const requestInit: RequestInit & { duplex: 'half' } = {
|
|
96
|
+
body: new ReadableStream({
|
|
97
|
+
start(controller) {
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
controller.enqueue(chunk)
|
|
100
|
+
}
|
|
101
|
+
controller.close()
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
duplex: 'half',
|
|
105
|
+
}
|
|
106
|
+
c.req.raw = new Request(c.req.raw, requestInit as RequestInit)
|
|
107
|
+
|
|
108
|
+
return next()
|
|
109
|
+
}
|
|
110
|
+
}
|