acrylblends 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +6 -1
- package/dist/index.js +84 -5
- package/dist/index.js.map +1 -1
- package/docs/framework/LICENSE +23 -0
- package/docs/framework/README.md +63 -0
- package/docs/framework/UPSTREAM.md +3 -0
- package/docs/framework/api-summary.md +138 -0
- package/docs/framework/concepts.md +105 -0
- package/docs/framework/context.md +129 -0
- package/docs/framework/getting-started.md +146 -0
- package/docs/framework/helpers.md +118 -0
- package/docs/framework/middleware.md +154 -0
- package/docs/framework/migration-from-0-3.md +108 -0
- package/docs/framework/migration-from-dsh-webserver.md +103 -0
- package/docs/framework/node-server.md +75 -0
- package/docs/framework/plugin-lifecycle.md +111 -0
- package/docs/framework/request.md +106 -0
- package/docs/framework/routing.md +188 -0
- package/docs/framework/surfaces.md +71 -0
- package/docs/framework/upstream.json +9 -0
- package/docs/framework/validation.md +135 -0
- package/package.json +4 -2
- package/plugins/acryl-inspect/plugin.json +2 -1
- package/plugins/acryl-inspect/src/report.ts +21 -13
- package/plugins/acryl-inspect/src/service.ts +9 -7
- package/plugins/acryl-inspect/tests/report.spec.ts +13 -8
- package/plugins/acryl-inspect/tests/service.spec.ts +11 -10
- package/plugins/acryl-inspect/upstream.json +2 -2
- package/plugins/acryl-node-server/LICENSE +26 -0
- package/plugins/acryl-node-server/plugin.json +2 -1
- package/plugins/acryl-node-server/src/adapter/conninfo.ts +23 -0
- package/plugins/acryl-node-server/src/adapter/early-hints.ts +55 -0
- package/plugins/acryl-node-server/src/adapter/error.ts +11 -0
- package/plugins/acryl-node-server/src/adapter/headers.ts +249 -0
- package/plugins/acryl-node-server/src/adapter/index.ts +6 -0
- package/plugins/acryl-node-server/src/adapter/listener.ts +540 -0
- package/plugins/acryl-node-server/src/adapter/request.ts +797 -0
- package/plugins/acryl-node-server/src/adapter/response.ts +186 -0
- package/plugins/acryl-node-server/src/adapter/serve-static.ts +238 -0
- package/plugins/acryl-node-server/src/adapter/server.ts +36 -0
- package/plugins/acryl-node-server/src/adapter/types.ts +82 -0
- package/plugins/acryl-node-server/src/adapter/url.ts +42 -0
- package/plugins/acryl-node-server/src/adapter/utils/response/constants.ts +1 -0
- package/plugins/acryl-node-server/src/adapter/utils/response.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/utils/stream.ts +88 -0
- package/plugins/acryl-node-server/src/adapter/utils.ts +96 -0
- package/plugins/acryl-node-server/src/adapter/websocket-types.ts +45 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +375 -0
- package/plugins/acryl-node-server/src/index.ts +19 -6
- package/plugins/acryl-node-server/tests/adapter/app.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/assets/.static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/favicon.ico +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/acryl.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/admin/secret.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/data.json +5 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/extensionless +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/foo..bar.txt +0 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/index.html +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-context-path/plain.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.bin.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.br +1 -0
- package/plugins/acryl-node-server/tests/adapter/assets/static-with-precompressed/hello.txt.zst +1 -0
- package/plugins/acryl-node-server/tests/adapter/conninfo.test.ts +34 -0
- package/plugins/acryl-node-server/tests/adapter/content-length.test.ts +248 -0
- package/plugins/acryl-node-server/tests/adapter/early-hints.test.ts +457 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-cert.pem +23 -0
- package/plugins/acryl-node-server/tests/adapter/fixtures/keys/agent1-key.pem +27 -0
- package/plugins/acryl-node-server/tests/adapter/headers.test.ts +252 -0
- package/plugins/acryl-node-server/tests/adapter/helpers/request.ts +190 -0
- package/plugins/acryl-node-server/tests/adapter/listener.test.ts +727 -0
- package/plugins/acryl-node-server/tests/adapter/request-client-disconnect.test.ts +189 -0
- package/plugins/acryl-node-server/tests/adapter/request.test.ts +1230 -0
- package/plugins/acryl-node-server/tests/adapter/response.test.ts +285 -0
- package/plugins/acryl-node-server/tests/adapter/serve-static.test.ts +849 -0
- package/plugins/acryl-node-server/tests/adapter/server-socket.test.ts +790 -0
- package/plugins/acryl-node-server/tests/adapter/server.test.ts +1302 -0
- package/plugins/acryl-node-server/tests/adapter/setup.ts +16 -0
- package/plugins/acryl-node-server/tests/adapter/url.test.ts +76 -0
- package/plugins/acryl-node-server/tests/adapter/utils/response.test.ts +23 -0
- package/plugins/acryl-node-server/tests/adapter/utils/stream.test.ts +75 -0
- package/plugins/acryl-node-server/tests/adapter/utils.test.ts +235 -0
- package/plugins/acryl-node-server/tests/adapter/websocket.test.ts +260 -0
- package/plugins/acryl-node-server/tests/server.spec.ts +44 -8
- package/plugins/acryl-node-server/upstream.json +3 -3
- package/plugins/acryl-router/LICENSE +26 -0
- package/plugins/acryl-router/plugin.json +2 -1
- package/plugins/acryl-router/src/client.ts +2 -0
- package/plugins/acryl-router/src/connection.ts +42 -0
- package/plugins/acryl-router/src/core/app-base.ts +544 -0
- package/plugins/acryl-router/src/core/app.test.ts +3833 -0
- package/plugins/acryl-router/src/core/app.ts +34 -0
- package/plugins/acryl-router/src/core/client/client.test.ts +2313 -0
- package/plugins/acryl-router/src/core/client/client.ts +249 -0
- package/plugins/acryl-router/src/core/client/fetch-result-please.ts +95 -0
- package/plugins/acryl-router/src/core/client/index.ts +17 -0
- package/plugins/acryl-router/src/core/client/types.test.ts +149 -0
- package/plugins/acryl-router/src/core/client/types.ts +393 -0
- package/plugins/acryl-router/src/core/client/utils.test.ts +405 -0
- package/plugins/acryl-router/src/core/client/utils.ts +117 -0
- package/plugins/acryl-router/src/core/compose.test.ts +751 -0
- package/plugins/acryl-router/src/core/compose.ts +73 -0
- package/plugins/acryl-router/src/core/context.test.ts +629 -0
- package/plugins/acryl-router/src/core/context.ts +781 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.test.ts +344 -0
- package/plugins/acryl-router/src/core/helper/accepts/accepts.ts +96 -0
- package/plugins/acryl-router/src/core/helper/accepts/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.test.ts +55 -0
- package/plugins/acryl-router/src/core/helper/adapter/index.ts +90 -0
- package/plugins/acryl-router/src/core/helper/conninfo/index.ts +6 -0
- package/plugins/acryl-router/src/core/helper/conninfo/types.ts +45 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.test.ts +510 -0
- package/plugins/acryl-router/src/core/helper/cookie/index.ts +146 -0
- package/plugins/acryl-router/src/core/helper/factory/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/helper/factory/index.ts +375 -0
- package/plugins/acryl-router/src/core/helper/html/index.test.ts +78 -0
- package/plugins/acryl-router/src/core/helper/html/index.ts +57 -0
- package/plugins/acryl-router/src/core/helper/route/index.test.ts +259 -0
- package/plugins/acryl-router/src/core/helper/route/index.ts +144 -0
- package/plugins/acryl-router/src/core/helper/streaming/index.ts +9 -0
- package/plugins/acryl-router/src/core/helper/streaming/sse.ts +101 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.test.ts +128 -0
- package/plugins/acryl-router/src/core/helper/streaming/stream.ts +45 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.test.ts +34 -0
- package/plugins/acryl-router/src/core/helper/streaming/text.ts +15 -0
- package/plugins/acryl-router/src/core/helper/streaming/utils.ts +11 -0
- package/plugins/acryl-router/src/core/helper/testing/index.test.ts +42 -0
- package/plugins/acryl-router/src/core/helper/testing/index.ts +28 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.test.ts +124 -0
- package/plugins/acryl-router/src/core/helper/websocket/index.ts +140 -0
- package/plugins/acryl-router/src/core/http-exception.test.ts +45 -0
- package/plugins/acryl-router/src/core/http-exception.ts +77 -0
- package/plugins/acryl-router/src/core/index.ts +53 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.test.ts +434 -0
- package/plugins/acryl-router/src/core/middleware/basic-auth/index.ts +152 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.test.ts +1018 -0
- package/plugins/acryl-router/src/core/middleware/bearer-auth/index.ts +220 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.test.ts +273 -0
- package/plugins/acryl-router/src/core/middleware/body-limit/index.ts +110 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.test.ts +355 -0
- package/plugins/acryl-router/src/core/middleware/combine/index.ts +171 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.test.ts +535 -0
- package/plugins/acryl-router/src/core/middleware/compress/index.ts +142 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.test.ts +43 -0
- package/plugins/acryl-router/src/core/middleware/context-storage/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.test.ts +532 -0
- package/plugins/acryl-router/src/core/middleware/cors/index.ts +163 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.test.ts +534 -0
- package/plugins/acryl-router/src/core/middleware/csrf/index.ts +150 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.test.ts +21 -0
- package/plugins/acryl-router/src/core/middleware/etag/digest.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.test.ts +503 -0
- package/plugins/acryl-router/src/core/middleware/etag/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.test.ts +217 -0
- package/plugins/acryl-router/src/core/middleware/ip-restriction/index.ts +278 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.test.ts +1135 -0
- package/plugins/acryl-router/src/core/middleware/jwk/index.ts +1 -0
- package/plugins/acryl-router/src/core/middleware/jwk/jwk.ts +195 -0
- package/plugins/acryl-router/src/core/middleware/jwk/keys.test.json +48 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.test.ts +802 -0
- package/plugins/acryl-router/src/core/middleware/jwt/index.ts +9 -0
- package/plugins/acryl-router/src/core/middleware/jwt/jwt.ts +189 -0
- package/plugins/acryl-router/src/core/middleware/language/index.test.ts +552 -0
- package/plugins/acryl-router/src/core/middleware/language/index.ts +12 -0
- package/plugins/acryl-router/src/core/middleware/language/language.ts +314 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.test.ts +204 -0
- package/plugins/acryl-router/src/core/middleware/logger/index.ts +95 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.test.ts +292 -0
- package/plugins/acryl-router/src/core/middleware/method-not-allowed/index.ts +141 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.test.ts +221 -0
- package/plugins/acryl-router/src/core/middleware/method-override/index.ts +147 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.test.ts +37 -0
- package/plugins/acryl-router/src/core/middleware/powered-by/index.ts +35 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.test.ts +218 -0
- package/plugins/acryl-router/src/core/middleware/pretty-json/index.ts +67 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.test.ts +168 -0
- package/plugins/acryl-router/src/core/middleware/request-id/index.ts +7 -0
- package/plugins/acryl-router/src/core/middleware/request-id/request-id.ts +59 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.test.ts +671 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/permissions-policy.ts +101 -0
- package/plugins/acryl-router/src/core/middleware/secure-headers/secure-headers.ts +340 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.test.ts +450 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/index.ts +135 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.test.ts +50 -0
- package/plugins/acryl-router/src/core/middleware/serve-static/path.ts +25 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.test.ts +70 -0
- package/plugins/acryl-router/src/core/middleware/timeout/index.ts +58 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.test.ts +310 -0
- package/plugins/acryl-router/src/core/middleware/timing/index.ts +8 -0
- package/plugins/acryl-router/src/core/middleware/timing/timing.ts +256 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.test.ts +381 -0
- package/plugins/acryl-router/src/core/middleware/trailing-slash/index.ts +156 -0
- package/plugins/acryl-router/src/core/request/constants.ts +1 -0
- package/plugins/acryl-router/src/core/request.test.ts +751 -0
- package/plugins/acryl-router/src/core/request.ts +502 -0
- package/plugins/acryl-router/src/core/router/common.case.test.ts +1132 -0
- package/plugins/acryl-router/src/core/router/linear-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.test.ts +80 -0
- package/plugins/acryl-router/src/core/router/linear-router/router.ts +152 -0
- package/plugins/acryl-router/src/core/router/pattern-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.test.ts +46 -0
- package/plugins/acryl-router/src/core/router/pattern-router/router.ts +54 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/index.ts +7 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/matcher.ts +33 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/node.ts +170 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.test.ts +154 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/prepared-router.ts +165 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.test.ts +265 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/router.ts +174 -0
- package/plugins/acryl-router/src/core/router/reg-exp-router/trie.ts +84 -0
- package/plugins/acryl-router/src/core/router/smart-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.test.ts +13 -0
- package/plugins/acryl-router/src/core/router/smart-router/router.ts +70 -0
- package/plugins/acryl-router/src/core/router/trie-router/index.ts +6 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.test.ts +896 -0
- package/plugins/acryl-router/src/core/router/trie-router/node.ts +221 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.test.ts +8 -0
- package/plugins/acryl-router/src/core/router/trie-router/router.ts +18 -0
- package/plugins/acryl-router/src/core/router/utils.ts +1 -0
- package/plugins/acryl-router/src/core/router.ts +103 -0
- package/plugins/acryl-router/src/core/types.test.ts +4134 -0
- package/plugins/acryl-router/src/core/types.ts +2782 -0
- package/plugins/acryl-router/src/core/utils/accept.test.ts +299 -0
- package/plugins/acryl-router/src/core/utils/accept.ts +268 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.test.ts +56 -0
- package/plugins/acryl-router/src/core/utils/basic-auth.ts +26 -0
- package/plugins/acryl-router/src/core/utils/body.test.ts +496 -0
- package/plugins/acryl-router/src/core/utils/body.ts +263 -0
- package/plugins/acryl-router/src/core/utils/buffer.test.ts +147 -0
- package/plugins/acryl-router/src/core/utils/buffer.ts +117 -0
- package/plugins/acryl-router/src/core/utils/color.test.ts +23 -0
- package/plugins/acryl-router/src/core/utils/color.ts +56 -0
- package/plugins/acryl-router/src/core/utils/compress.ts +10 -0
- package/plugins/acryl-router/src/core/utils/concurrent.test.ts +69 -0
- package/plugins/acryl-router/src/core/utils/concurrent.ts +55 -0
- package/plugins/acryl-router/src/core/utils/constants.ts +4 -0
- package/plugins/acryl-router/src/core/utils/cookie.test.ts +460 -0
- package/plugins/acryl-router/src/core/utils/cookie.ts +281 -0
- package/plugins/acryl-router/src/core/utils/crypto.test.ts +43 -0
- package/plugins/acryl-router/src/core/utils/crypto.ts +58 -0
- package/plugins/acryl-router/src/core/utils/encode.test.ts +61 -0
- package/plugins/acryl-router/src/core/utils/encode.ts +34 -0
- package/plugins/acryl-router/src/core/utils/filepath.test.ts +78 -0
- package/plugins/acryl-router/src/core/utils/filepath.ts +60 -0
- package/plugins/acryl-router/src/core/utils/handler.ts +15 -0
- package/plugins/acryl-router/src/core/utils/headers.ts +355 -0
- package/plugins/acryl-router/src/core/utils/html.test.ts +16 -0
- package/plugins/acryl-router/src/core/utils/html.ts +182 -0
- package/plugins/acryl-router/src/core/utils/http-status.ts +72 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.test.ts +149 -0
- package/plugins/acryl-router/src/core/utils/ipaddr.ts +374 -0
- package/plugins/acryl-router/src/core/utils/jwt/index.ts +7 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.test.ts +86 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwa.ts +38 -0
- package/plugins/acryl-router/src/core/utils/jwt/jws.ts +235 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.test.ts +1788 -0
- package/plugins/acryl-router/src/core/utils/jwt/jwt.ts +292 -0
- package/plugins/acryl-router/src/core/utils/jwt/types.ts +157 -0
- package/plugins/acryl-router/src/core/utils/jwt/utf8.ts +7 -0
- package/plugins/acryl-router/src/core/utils/mime.test.ts +64 -0
- package/plugins/acryl-router/src/core/utils/mime.ts +94 -0
- package/plugins/acryl-router/src/core/utils/stream.test.ts +212 -0
- package/plugins/acryl-router/src/core/utils/stream.ts +107 -0
- package/plugins/acryl-router/src/core/utils/types.test.ts +295 -0
- package/plugins/acryl-router/src/core/utils/types.ts +123 -0
- package/plugins/acryl-router/src/core/utils/url.test.ts +384 -0
- package/plugins/acryl-router/src/core/utils/url.ts +337 -0
- package/plugins/acryl-router/src/core/validator/index.ts +8 -0
- package/plugins/acryl-router/src/core/validator/utils.test.ts +82 -0
- package/plugins/acryl-router/src/core/validator/utils.ts +69 -0
- package/plugins/acryl-router/src/core/validator/validator.test.ts +1488 -0
- package/plugins/acryl-router/src/core/validator/validator.ts +185 -0
- package/plugins/acryl-router/src/helpers/index.ts +13 -3
- package/plugins/acryl-router/src/helpers/schema-validator.ts +35 -0
- package/plugins/acryl-router/src/index.ts +6 -6
- package/plugins/acryl-router/src/middleware/index.ts +25 -6
- package/plugins/acryl-router/src/middleware/loopback-only.ts +53 -14
- package/plugins/acryl-router/src/service.ts +262 -64
- package/plugins/acryl-router/tests/docs-examples.spec.ts +152 -0
- package/plugins/acryl-router/tests/live-install.spec.ts +2 -2
- package/plugins/acryl-router/tests/loader.spec.ts +3 -2
- package/plugins/acryl-router/tests/loopback-only.spec.ts +80 -0
- package/plugins/acryl-router/tests/service.spec.ts +281 -66
- package/plugins/acryl-router/tests/shadow-state.spec.ts +79 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/plugin.json +2 -1
- package/plugins/acryl-router-http/src/bridge.ts +3 -1
- package/plugins/acryl-router-http/tests/adapter.spec.ts +16 -12
- package/plugins/acryl-router-http/tests/surfaces.spec.ts +12 -10
- package/plugins/acryl-router-http/upstream.json +3 -3
- package/plugins/acryl-state/NOTICE +30 -0
- package/plugins/acryl-state/plugin.json +1 -1
- package/plugins/acryl-state/src/cell.ts +1 -1
- package/plugins/acryl-state/src/service.ts +33 -15
- package/plugins/acryl-state/upstream.json +6 -4
- package/plugins/builder/script.ts +4 -7
- package/plugins/extensions/index.ts +6 -5
- package/plugins/extensions/resolve.ts +28 -3
- package/plugins/openai/prompt.ts +9 -7
- package/plugins/tsconfig.json +5 -4
- package/shared/AGENTS.core.md +54 -0
- package/templates/agent/AGENTS.md +6 -0
- package/templates/agent/README.md +2 -2
- package/templates/agent/blend.yaml +1 -1
- package/templates/agent/src/routes.ts +37 -27
- package/templates/agent/test/model.test.ts +2 -1
- package/templates/agent/tsconfig.json +8 -5
- package/templates/blank/AGENTS.md +5 -0
- package/templates/blank/README.md +1 -1
- package/templates/blank/src/routes.ts +6 -4
- package/templates/blank/tsconfig.json +2 -4
- package/templates/todo/AGENTS.md +5 -0
- package/templates/todo/src/page.ts +1 -1
- package/templates/todo/src/todo.ts +58 -29
- package/templates/todo/tsconfig.json +2 -4
- package/templates/todo/upstream.json +2 -2
- package/plugins/acryl-router/src/compose.ts +0 -21
- package/plugins/acryl-router/src/context.ts +0 -59
- package/plugins/acryl-router/src/helpers/read-body.ts +0 -59
- package/plugins/acryl-router/src/helpers/sse.ts +0 -83
- package/plugins/acryl-router/src/helpers/validate.ts +0 -44
- package/plugins/acryl-router/src/http-error.ts +0 -21
- package/plugins/acryl-router/src/middleware/body-limit.ts +0 -25
- package/plugins/acryl-router/src/middleware/cors.ts +0 -46
- package/plugins/acryl-router/src/middleware/logger.ts +0 -21
- package/plugins/acryl-router/src/middleware/request-id.ts +0 -24
- package/plugins/acryl-router/src/middleware/secure-headers.ts +0 -23
- package/plugins/acryl-router/src/pattern.ts +0 -89
- package/plugins/acryl-router/src/registrar.ts +0 -90
- package/plugins/acryl-router/src/table.ts +0 -156
- package/plugins/acryl-router/src/types.ts +0 -73
- package/plugins/acryl-router/src/util.ts +0 -23
- package/plugins/acryl-router/tests/api.spec.ts +0 -223
- package/plugins/acryl-router/tests/conformance.ts +0 -170
- package/plugins/acryl-router/tests/helpers.spec.ts +0 -172
- package/plugins/acryl-router/tests/middleware.spec.ts +0 -183
- package/plugins/acryl-router/tests/table.spec.ts +0 -47
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import * as z from 'zod'
|
|
3
|
+
import type { Context } from '../context.ts'
|
|
4
|
+
import { App } from '../app.ts'
|
|
5
|
+
import { HTTPException } from '../http-exception.ts'
|
|
6
|
+
import { cloneRawRequest } from '../request.ts'
|
|
7
|
+
import type {
|
|
8
|
+
ErrorHandler,
|
|
9
|
+
ExtractSchema,
|
|
10
|
+
ExtractSchemaForStatusCode,
|
|
11
|
+
FormValue,
|
|
12
|
+
MiddlewareHandler,
|
|
13
|
+
TypedResponse,
|
|
14
|
+
ValidationTargets,
|
|
15
|
+
} from '../types.ts'
|
|
16
|
+
import type { ContentfulStatusCode, StatusCode } from '../utils/http-status.ts'
|
|
17
|
+
import type { Equal, Expect } from '../utils/types.ts'
|
|
18
|
+
import type { ValidationFunction } from './validator.ts'
|
|
19
|
+
import { validator } from './validator.ts'
|
|
20
|
+
|
|
21
|
+
// Helper type to extract the response type from the validation function
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
type InferValidatorResponse<VF> = VF extends (value: any, c: any) => infer R
|
|
24
|
+
? R extends Promise<infer PR>
|
|
25
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
PR extends Response | TypedResponse<any, any, any>
|
|
27
|
+
? PR
|
|
28
|
+
: never
|
|
29
|
+
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
R extends Response | TypedResponse<any, any, any>
|
|
31
|
+
? R
|
|
32
|
+
: never
|
|
33
|
+
: never
|
|
34
|
+
|
|
35
|
+
// Reference implementation for only testing
|
|
36
|
+
const zodValidator = <
|
|
37
|
+
T extends z.ZodSchema,
|
|
38
|
+
E extends {},
|
|
39
|
+
P extends string,
|
|
40
|
+
Target extends keyof ValidationTargets,
|
|
41
|
+
>(
|
|
42
|
+
target: Target,
|
|
43
|
+
schema: T
|
|
44
|
+
) => {
|
|
45
|
+
const validationFunc = (value: unknown, c: Context<E, P>) => {
|
|
46
|
+
const result = schema.safeParse(value)
|
|
47
|
+
if (!result.success) {
|
|
48
|
+
return c.text('Invalid!', 400)
|
|
49
|
+
}
|
|
50
|
+
return result.data as z.output<T>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type ResponseType = InferValidatorResponse<typeof validationFunc>
|
|
54
|
+
|
|
55
|
+
return validator(target, validationFunc) as MiddlewareHandler<
|
|
56
|
+
E,
|
|
57
|
+
P,
|
|
58
|
+
{ in: { [K in Target]: z.input<T> }; out: { [K in Target]: z.output<T> } },
|
|
59
|
+
ResponseType
|
|
60
|
+
>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('Basic', () => {
|
|
64
|
+
const app = new App()
|
|
65
|
+
|
|
66
|
+
const route = app.get(
|
|
67
|
+
'/search',
|
|
68
|
+
async (_c, next) => {
|
|
69
|
+
await next()
|
|
70
|
+
},
|
|
71
|
+
validator('query', (value, c) => {
|
|
72
|
+
type verify = Expect<Equal<Record<string, string | string[]>, typeof value>>
|
|
73
|
+
if (!value.q) {
|
|
74
|
+
return c.text('Invalid!', 400)
|
|
75
|
+
}
|
|
76
|
+
}),
|
|
77
|
+
(c) => {
|
|
78
|
+
return c.text('Valid!', 200)
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
type Expected = {
|
|
83
|
+
'/search': {
|
|
84
|
+
$get:
|
|
85
|
+
| {
|
|
86
|
+
input: {
|
|
87
|
+
query: {}
|
|
88
|
+
}
|
|
89
|
+
output: 'Invalid!'
|
|
90
|
+
outputFormat: 'text'
|
|
91
|
+
status: 400
|
|
92
|
+
}
|
|
93
|
+
| {
|
|
94
|
+
input: {
|
|
95
|
+
query: {}
|
|
96
|
+
}
|
|
97
|
+
output: 'Valid!'
|
|
98
|
+
outputFormat: 'text'
|
|
99
|
+
status: 200
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Actual = ExtractSchema<typeof route>
|
|
105
|
+
|
|
106
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
107
|
+
|
|
108
|
+
it('Should return 200 response', async () => {
|
|
109
|
+
const res = await app.request('http://localhost/search?q=foo')
|
|
110
|
+
expect(res.status).toBe(200)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('Should return 400 response', async () => {
|
|
114
|
+
const res = await app.request('http://localhost/search')
|
|
115
|
+
expect(res.status).toBe(400)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const onErrorHandler: ErrorHandler = (e, c) => {
|
|
120
|
+
if (e instanceof HTTPException) {
|
|
121
|
+
return c.json({ message: e.message, success: false }, e.status)
|
|
122
|
+
}
|
|
123
|
+
return c.json({ message: e.message }, 500)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
describe('JSON', () => {
|
|
127
|
+
const app = new App()
|
|
128
|
+
app.post(
|
|
129
|
+
'/post',
|
|
130
|
+
validator('json', (value) => value),
|
|
131
|
+
(c) => {
|
|
132
|
+
return c.json(c.req.valid('json'))
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
it('Should return 200 response with a valid JSON data', async () => {
|
|
137
|
+
const res = await app.request('http://localhost/post', {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
140
|
+
headers: {
|
|
141
|
+
'Content-Type': 'application/json',
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
expect(res.status).toBe(200)
|
|
145
|
+
const data = await res.json()
|
|
146
|
+
expect(data).toEqual({ foo: 'bar' })
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('Should validate if Content-Type media type is mixed-case', async () => {
|
|
150
|
+
const res = await app.request('http://localhost/post', {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
153
|
+
headers: {
|
|
154
|
+
'Content-Type': 'Application/JSON',
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
expect(res.status).toBe(200)
|
|
158
|
+
expect(await res.json()).toEqual({ foo: 'bar' })
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('Should not validate if Content-Type is not set', async () => {
|
|
162
|
+
const res = await app.request('http://localhost/post', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
165
|
+
})
|
|
166
|
+
expect(res.status).toBe(200)
|
|
167
|
+
const data = await res.json()
|
|
168
|
+
expect(data.foo).toBeUndefined()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('Should not validate if Content-Type is wrong', async () => {
|
|
172
|
+
const res = await app.request('http://localhost/post', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: {
|
|
175
|
+
'Content-Type': 'text/plain;charset=utf-8',
|
|
176
|
+
},
|
|
177
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
178
|
+
})
|
|
179
|
+
expect(res.status).toBe(200)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('Should validate if Content-Type is a application/json with a charset', async () => {
|
|
183
|
+
const res = await app.request('http://localhost/post', {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
'Content-Type': 'application/json',
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
189
|
+
})
|
|
190
|
+
expect(res.status).toBe(200)
|
|
191
|
+
expect(await res.json()).toEqual({ foo: 'bar' })
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('Should validate if Content-Type is a subtype like application/merge-patch+json', async () => {
|
|
195
|
+
const res = await app.request('http://localhost/post', {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: {
|
|
198
|
+
'Content-Type': 'application/merge-patch+json',
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
201
|
+
})
|
|
202
|
+
expect(res.status).toBe(200)
|
|
203
|
+
expect(await res.json()).toEqual({ foo: 'bar' })
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('Should validate if Content-Type is application/vnd.api+json', async () => {
|
|
207
|
+
const res = await app.request('http://localhost/post', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/vnd.api+json',
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
213
|
+
})
|
|
214
|
+
expect(res.status).toBe(200)
|
|
215
|
+
expect(await res.json()).toEqual({ foo: 'bar' })
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('Should not validate if Content-Type does not start with application/json', async () => {
|
|
219
|
+
const res = await app.request('http://localhost/post', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: {
|
|
222
|
+
'Content-Type': 'Xapplication/json',
|
|
223
|
+
},
|
|
224
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
225
|
+
})
|
|
226
|
+
expect(res.status).toBe(200)
|
|
227
|
+
const data = await res.json()
|
|
228
|
+
expect(data.foo).toBeUndefined()
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('Malformed JSON', () => {
|
|
233
|
+
const app = new App()
|
|
234
|
+
app.post(
|
|
235
|
+
'/post',
|
|
236
|
+
validator('json', (value) => value),
|
|
237
|
+
(c) => {
|
|
238
|
+
return c.json(c.req.valid('json'))
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
it('Should return 400 response if the body data is not a valid JSON', async () => {
|
|
243
|
+
const formData = new FormData()
|
|
244
|
+
formData.append('foo', 'bar')
|
|
245
|
+
const res = await app.request('http://localhost/post', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/json',
|
|
249
|
+
},
|
|
250
|
+
body: formData,
|
|
251
|
+
})
|
|
252
|
+
expect(res.status).toBe(400)
|
|
253
|
+
})
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
describe('FormData', () => {
|
|
257
|
+
const app = new App()
|
|
258
|
+
app.post(
|
|
259
|
+
'/post',
|
|
260
|
+
validator('form', (value) => value),
|
|
261
|
+
(c) => {
|
|
262
|
+
return c.json(c.req.valid('form'))
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
it('Should return 200 response with a valid form data', async () => {
|
|
267
|
+
const formData = new FormData()
|
|
268
|
+
formData.append('message', 'hi')
|
|
269
|
+
const res = await app.request('http://localhost/post', {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
body: formData,
|
|
272
|
+
})
|
|
273
|
+
expect(res.status).toBe(200)
|
|
274
|
+
expect(await res.json()).toEqual({ message: 'hi' })
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('Should validate a URL Encoded Data', async () => {
|
|
278
|
+
const params = new URLSearchParams()
|
|
279
|
+
params.append('foo', 'bar')
|
|
280
|
+
const res = await app.request('/post', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
body: params,
|
|
283
|
+
headers: {
|
|
284
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
expect(res.status).toBe(200)
|
|
288
|
+
expect(await res.json()).toEqual({
|
|
289
|
+
foo: 'bar',
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('Should validate mixed-case URL Encoded Content-Type', async () => {
|
|
294
|
+
const params = new URLSearchParams()
|
|
295
|
+
params.append('foo', 'bar')
|
|
296
|
+
const res = await app.request('/post', {
|
|
297
|
+
method: 'POST',
|
|
298
|
+
body: params,
|
|
299
|
+
headers: {
|
|
300
|
+
'content-type': 'Application/X-WWW-Form-Urlencoded; charset=UTF-8',
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
expect(res.status).toBe(200)
|
|
304
|
+
expect(await res.json()).toEqual({
|
|
305
|
+
foo: 'bar',
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('Should validate mixed-case multipart/form-data Content-Type', async () => {
|
|
310
|
+
const formData = new FormData()
|
|
311
|
+
formData.append('foo', 'bar')
|
|
312
|
+
const source = new Request('http://localhost/post', { method: 'POST', body: formData })
|
|
313
|
+
const contentType = source.headers
|
|
314
|
+
.get('Content-Type')!
|
|
315
|
+
.replace('multipart/form-data', 'Multipart/Form-Data')
|
|
316
|
+
const res = await app.request('/post', {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
body: await source.arrayBuffer(),
|
|
319
|
+
headers: {
|
|
320
|
+
'content-type': contentType,
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
expect(res.status).toBe(200)
|
|
324
|
+
expect(await res.json()).toEqual({
|
|
325
|
+
foo: 'bar',
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('Should validate if Content-Type is a application/x-www-form-urlencoded with a charset', async () => {
|
|
330
|
+
const params = new URLSearchParams()
|
|
331
|
+
params.append('foo', 'bar')
|
|
332
|
+
const res = await app.request('/post', {
|
|
333
|
+
method: 'POST',
|
|
334
|
+
body: params,
|
|
335
|
+
headers: {
|
|
336
|
+
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
expect(res.status).toBe(200)
|
|
340
|
+
expect(await res.json()).toEqual({
|
|
341
|
+
foo: 'bar',
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('Should return `foo[]` as an array', async () => {
|
|
346
|
+
const form = new FormData()
|
|
347
|
+
form.append('foo[]', 'bar1')
|
|
348
|
+
form.append('foo[]', 'bar2')
|
|
349
|
+
const res = await app.request('/post', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
body: form,
|
|
352
|
+
})
|
|
353
|
+
expect(res.status).toBe(200)
|
|
354
|
+
expect(await res.json()).toEqual({
|
|
355
|
+
'foo[]': ['bar1', 'bar2'],
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('Should return `foo` as an array if multiple values are appended', async () => {
|
|
360
|
+
const form = new FormData()
|
|
361
|
+
form.append('foo', 'bar1')
|
|
362
|
+
form.append('foo', 'bar2')
|
|
363
|
+
form.append('foo', 'bar3')
|
|
364
|
+
const res = await app.request('/post', {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
body: form,
|
|
367
|
+
})
|
|
368
|
+
expect(res.status).toBe(200)
|
|
369
|
+
expect(await res.json()).toEqual({
|
|
370
|
+
foo: ['bar1', 'bar2', 'bar3'],
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
describe('Malformed FormData request', () => {
|
|
376
|
+
const app = new App()
|
|
377
|
+
app.post(
|
|
378
|
+
'/post',
|
|
379
|
+
validator('form', (value) => value),
|
|
380
|
+
(c) => {
|
|
381
|
+
return c.json(c.req.valid('form'))
|
|
382
|
+
}
|
|
383
|
+
)
|
|
384
|
+
app.onError(onErrorHandler)
|
|
385
|
+
|
|
386
|
+
it('Should return 400 response, for malformed content type header', async () => {
|
|
387
|
+
const res = await app.request('http://localhost/post', {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
body: 'hi',
|
|
390
|
+
headers: {
|
|
391
|
+
'content-type': 'multipart/form-data',
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
expect(res.status).toBe(400)
|
|
395
|
+
const data = await res.json()
|
|
396
|
+
expect(data['success']).toBe(false)
|
|
397
|
+
expect(data['message']).toMatch(/^Malformed FormData request./)
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('JSON and FormData', () => {
|
|
402
|
+
const app = new App()
|
|
403
|
+
app.post(
|
|
404
|
+
'/',
|
|
405
|
+
validator('json', (value) => value),
|
|
406
|
+
validator('form', (value) => value),
|
|
407
|
+
async (c) => {
|
|
408
|
+
const jsonData = c.req.valid('json')
|
|
409
|
+
const formData = c.req.valid('form')
|
|
410
|
+
return c.json({
|
|
411
|
+
json: jsonData,
|
|
412
|
+
form: formData,
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
it('Should validate a JSON request', async () => {
|
|
418
|
+
const res = await app.request('/', {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
421
|
+
headers: {
|
|
422
|
+
'Content-Type': 'application/json',
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
expect(res.status).toBe(200)
|
|
426
|
+
const data = await res.json()
|
|
427
|
+
expect(data.json).toEqual({ foo: 'bar' })
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('Should validate a FormData request', async () => {
|
|
431
|
+
const form = new FormData()
|
|
432
|
+
form.append('foo', 'bar')
|
|
433
|
+
const res = await app.request('/', {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
body: form,
|
|
436
|
+
})
|
|
437
|
+
expect(res.status).toBe(200)
|
|
438
|
+
const data = await res.json()
|
|
439
|
+
expect(data.form).toEqual({ foo: 'bar' })
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
describe('Cached contents', () => {
|
|
444
|
+
describe('json', () => {
|
|
445
|
+
const app = new App()
|
|
446
|
+
const bodyTypes = ['json', 'text', 'arrayBuffer', 'blob']
|
|
447
|
+
|
|
448
|
+
for (const type of bodyTypes) {
|
|
449
|
+
app.post(
|
|
450
|
+
`/${type}`,
|
|
451
|
+
async (c, next) => {
|
|
452
|
+
// @ts-expect-error not type safe
|
|
453
|
+
await c.req[type]()
|
|
454
|
+
await next()
|
|
455
|
+
},
|
|
456
|
+
validator('json', (value) => {
|
|
457
|
+
if (value instanceof Promise) {
|
|
458
|
+
throw new Error('Value is Promise')
|
|
459
|
+
}
|
|
460
|
+
return value
|
|
461
|
+
}),
|
|
462
|
+
async (c) => {
|
|
463
|
+
const data = await c.req.json()
|
|
464
|
+
return c.json(data, 200)
|
|
465
|
+
}
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
for (const type of bodyTypes) {
|
|
470
|
+
const endpoint = `/${type}`
|
|
471
|
+
it(`Should return the cached JSON content - ${endpoint}`, async () => {
|
|
472
|
+
const res = await app.request(endpoint, {
|
|
473
|
+
method: 'POST',
|
|
474
|
+
body: JSON.stringify({ message: 'Hello' }),
|
|
475
|
+
headers: {
|
|
476
|
+
'Content-Type': 'application/json',
|
|
477
|
+
},
|
|
478
|
+
})
|
|
479
|
+
expect(res.status).toBe(200)
|
|
480
|
+
expect(await res.json()).toEqual({ message: 'Hello' })
|
|
481
|
+
})
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
describe('Cached content', () => {
|
|
486
|
+
const app = new App()
|
|
487
|
+
const bodyTypes = ['formData', 'text', 'arrayBuffer', 'blob']
|
|
488
|
+
|
|
489
|
+
for (const type of bodyTypes) {
|
|
490
|
+
app.post(
|
|
491
|
+
`/${type}`,
|
|
492
|
+
async (c, next) => {
|
|
493
|
+
// @ts-expect-error not type safe
|
|
494
|
+
await c.req[type]()
|
|
495
|
+
await next()
|
|
496
|
+
},
|
|
497
|
+
validator('form', (value) => {
|
|
498
|
+
if (value instanceof Promise) {
|
|
499
|
+
throw new Error('Value is Promise')
|
|
500
|
+
}
|
|
501
|
+
return value
|
|
502
|
+
}),
|
|
503
|
+
async (c) => {
|
|
504
|
+
return c.json({ message: 'OK' }, 200)
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const type of bodyTypes) {
|
|
510
|
+
const endpoint = `/${type}`
|
|
511
|
+
it(`Should return the cached FormData content - ${endpoint}`, async () => {
|
|
512
|
+
const form = new FormData()
|
|
513
|
+
form.append('message', 'Hello')
|
|
514
|
+
const res = await app.request(endpoint, {
|
|
515
|
+
method: 'POST',
|
|
516
|
+
body: form,
|
|
517
|
+
})
|
|
518
|
+
expect(res.status).toBe(200)
|
|
519
|
+
expect(await res.json()).toEqual({ message: 'OK' })
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
})
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
describe('Validator middleware with a custom validation function', () => {
|
|
526
|
+
const app = new App()
|
|
527
|
+
|
|
528
|
+
const validationFunction: ValidationFunction<{ id: string }, { id: number }> = (v) => {
|
|
529
|
+
return {
|
|
530
|
+
id: Number(v.id),
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const route = app.post('/post', validator('json', validationFunction), (c) => {
|
|
535
|
+
const post = c.req.valid('json')
|
|
536
|
+
type Expected = {
|
|
537
|
+
id: number
|
|
538
|
+
}
|
|
539
|
+
type verify = Expect<Equal<Expected, typeof post>>
|
|
540
|
+
return c.json({
|
|
541
|
+
post,
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
type Expected = {
|
|
546
|
+
'/post': {
|
|
547
|
+
$post:
|
|
548
|
+
| {
|
|
549
|
+
input: {
|
|
550
|
+
json: {
|
|
551
|
+
id: number
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
output: {}
|
|
555
|
+
outputFormat: string
|
|
556
|
+
status: StatusCode
|
|
557
|
+
}
|
|
558
|
+
| {
|
|
559
|
+
input: {
|
|
560
|
+
json: {
|
|
561
|
+
id: number
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
output: {
|
|
565
|
+
post: {
|
|
566
|
+
id: number
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
outputFormat: 'json'
|
|
570
|
+
status: ContentfulStatusCode
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
type Actual = ExtractSchema<typeof route>
|
|
576
|
+
type verify2 = Expect<Equal<Expected, Actual>>
|
|
577
|
+
|
|
578
|
+
it('Should validate JSON with transformation and return 200 response', async () => {
|
|
579
|
+
const res = await app.request('http://localhost/post', {
|
|
580
|
+
method: 'POST',
|
|
581
|
+
headers: {
|
|
582
|
+
'Content-Type': 'application/json',
|
|
583
|
+
},
|
|
584
|
+
body: JSON.stringify({
|
|
585
|
+
id: '123',
|
|
586
|
+
}),
|
|
587
|
+
})
|
|
588
|
+
expect(res.status).toBe(200)
|
|
589
|
+
expect(await res.json()).toEqual({
|
|
590
|
+
post: {
|
|
591
|
+
id: 123,
|
|
592
|
+
},
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
describe('Validator middleware with Zod validates JSON', () => {
|
|
598
|
+
const app = new App()
|
|
599
|
+
|
|
600
|
+
const schema = z.object({
|
|
601
|
+
id: z.number(),
|
|
602
|
+
title: z.string(),
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
const route = app.post('/post', zodValidator('json', schema), (c) => {
|
|
606
|
+
const post = c.req.valid('json')
|
|
607
|
+
type Expected = {
|
|
608
|
+
id: number
|
|
609
|
+
title: string
|
|
610
|
+
}
|
|
611
|
+
type verify = Expect<Equal<Expected, typeof post>>
|
|
612
|
+
return c.json({
|
|
613
|
+
post: post,
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
type Expected = {
|
|
618
|
+
'/post': {
|
|
619
|
+
$post:
|
|
620
|
+
| {
|
|
621
|
+
input: {
|
|
622
|
+
json: {
|
|
623
|
+
id: number
|
|
624
|
+
title: string
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
output: 'Invalid!'
|
|
628
|
+
outputFormat: 'text'
|
|
629
|
+
status: 400
|
|
630
|
+
}
|
|
631
|
+
| {
|
|
632
|
+
input: {
|
|
633
|
+
json: {
|
|
634
|
+
id: number
|
|
635
|
+
title: string
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
output: {
|
|
639
|
+
post: {
|
|
640
|
+
id: number
|
|
641
|
+
title: string
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
outputFormat: 'json'
|
|
645
|
+
status: ContentfulStatusCode
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
type Actual = ExtractSchema<typeof route>
|
|
651
|
+
|
|
652
|
+
type verify2 = Expect<Equal<Expected, Actual>>
|
|
653
|
+
|
|
654
|
+
it('Should validate JSON and return 200 response', async () => {
|
|
655
|
+
const res = await app.request('http://localhost/post', {
|
|
656
|
+
method: 'POST',
|
|
657
|
+
headers: {
|
|
658
|
+
'Content-Type': 'application/json',
|
|
659
|
+
},
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
id: 123,
|
|
662
|
+
title: 'Hello',
|
|
663
|
+
}),
|
|
664
|
+
})
|
|
665
|
+
expect(res.status).toBe(200)
|
|
666
|
+
expect(await res.json()).toEqual({
|
|
667
|
+
post: {
|
|
668
|
+
id: 123,
|
|
669
|
+
title: 'Hello',
|
|
670
|
+
},
|
|
671
|
+
})
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
it('Should validate JSON and return 400 response', async () => {
|
|
675
|
+
const res = await app.request('http://localhost/post', {
|
|
676
|
+
method: 'POST',
|
|
677
|
+
headers: {
|
|
678
|
+
'Content-Type': 'application/json',
|
|
679
|
+
},
|
|
680
|
+
body: JSON.stringify({
|
|
681
|
+
id: '123',
|
|
682
|
+
title: 'Hello',
|
|
683
|
+
}),
|
|
684
|
+
})
|
|
685
|
+
expect(res.status).toBe(400)
|
|
686
|
+
expect(await res.text()).toBe('Invalid!')
|
|
687
|
+
})
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
describe('Validator middleware with Zod validates Form data', () => {
|
|
691
|
+
const app = new App()
|
|
692
|
+
|
|
693
|
+
const schema = z.object({
|
|
694
|
+
id: z.string(),
|
|
695
|
+
title: z.string(),
|
|
696
|
+
})
|
|
697
|
+
app.post('/post', zodValidator('form', schema), (c) => {
|
|
698
|
+
const post = c.req.valid('form')
|
|
699
|
+
return c.json({
|
|
700
|
+
post: post,
|
|
701
|
+
})
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
it('Should validate Form data and return 200 response', async () => {
|
|
705
|
+
const form = new FormData()
|
|
706
|
+
form.append('id', '123')
|
|
707
|
+
form.append('title', 'Hello')
|
|
708
|
+
const res = await app.request('http://localhost/post', {
|
|
709
|
+
method: 'POST',
|
|
710
|
+
body: form,
|
|
711
|
+
})
|
|
712
|
+
expect(res.status).toBe(200)
|
|
713
|
+
expect(await res.json()).toEqual({
|
|
714
|
+
post: {
|
|
715
|
+
id: '123',
|
|
716
|
+
title: 'Hello',
|
|
717
|
+
},
|
|
718
|
+
})
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
it('Should validate Form data and return 400 response', async () => {
|
|
722
|
+
const res = await app.request('http://localhost/post', {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
})
|
|
725
|
+
expect(res.status).toBe(400)
|
|
726
|
+
expect(await res.text()).toBe('Invalid!')
|
|
727
|
+
})
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
describe('Validator middleware with Zod validates query params', () => {
|
|
731
|
+
const app = new App()
|
|
732
|
+
|
|
733
|
+
const schema = z.object({
|
|
734
|
+
page: z
|
|
735
|
+
.string()
|
|
736
|
+
.refine((v) => {
|
|
737
|
+
return !isNaN(Number(v))
|
|
738
|
+
})
|
|
739
|
+
.transform((v) => {
|
|
740
|
+
return Number(v)
|
|
741
|
+
}),
|
|
742
|
+
tag: z.array(z.string()),
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
app.get('/search', zodValidator('query', schema), (c) => {
|
|
746
|
+
const res = c.req.valid('query')
|
|
747
|
+
return c.json({
|
|
748
|
+
page: res.page,
|
|
749
|
+
tags: res.tag,
|
|
750
|
+
})
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it('Should validate query params and return 200 response', async () => {
|
|
754
|
+
const res = await app.request('http://localhost/search?page=123&tag=a&tag=b')
|
|
755
|
+
expect(res.status).toBe(200)
|
|
756
|
+
expect(await res.json()).toEqual({
|
|
757
|
+
page: 123,
|
|
758
|
+
tags: ['a', 'b'],
|
|
759
|
+
})
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('Should validate query params and return 400 response', async () => {
|
|
763
|
+
const res = await app.request('http://localhost/search?page=onetwothree')
|
|
764
|
+
expect(res.status).toBe(400)
|
|
765
|
+
expect(await res.text()).toBe('Invalid!')
|
|
766
|
+
})
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
describe('Validator middleware with Zod validates param', () => {
|
|
770
|
+
const app = new App()
|
|
771
|
+
|
|
772
|
+
const schema = z.object({
|
|
773
|
+
id: z
|
|
774
|
+
.string()
|
|
775
|
+
.regex(/^[0-9]+$/)
|
|
776
|
+
.transform((v) => {
|
|
777
|
+
return Number(v)
|
|
778
|
+
}),
|
|
779
|
+
title: z.string(),
|
|
780
|
+
})
|
|
781
|
+
app.get('/users/:id/books/:title', zodValidator('param', schema), (c) => {
|
|
782
|
+
const param = c.req.valid('param')
|
|
783
|
+
return c.json({
|
|
784
|
+
param: param,
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('Should validate Form data and return 200 response', async () => {
|
|
789
|
+
const res = await app.request('http://localhost/users/123/books/Hello')
|
|
790
|
+
expect(res.status).toBe(200)
|
|
791
|
+
expect(await res.json()).toEqual({
|
|
792
|
+
param: {
|
|
793
|
+
id: 123,
|
|
794
|
+
title: 'Hello',
|
|
795
|
+
},
|
|
796
|
+
})
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
it('Should validate Form data and return 400 response', async () => {
|
|
800
|
+
const res = await app.request('http://localhost/users/0.123/books/Hello')
|
|
801
|
+
expect(res.status).toBe(400)
|
|
802
|
+
expect(await res.text()).toBe('Invalid!')
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
describe('Validator middleware with Zod validates header values', () => {
|
|
807
|
+
const app = new App()
|
|
808
|
+
|
|
809
|
+
const schema = z.object({
|
|
810
|
+
'x-request-id': z.string().uuid(),
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
app.get('/ping', zodValidator('header', schema), (c) => {
|
|
814
|
+
const data = c.req.valid('header')
|
|
815
|
+
const xRequestId = data['x-request-id']
|
|
816
|
+
return c.json({
|
|
817
|
+
xRequestId,
|
|
818
|
+
})
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
it('Should validate header values and return 200 response', async () => {
|
|
822
|
+
const res = await app.request('/ping', {
|
|
823
|
+
headers: {
|
|
824
|
+
'x-request-id': '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b',
|
|
825
|
+
},
|
|
826
|
+
})
|
|
827
|
+
expect(res.status).toBe(200)
|
|
828
|
+
expect(await res.json()).toEqual({
|
|
829
|
+
xRequestId: '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b',
|
|
830
|
+
})
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('Should validate header values and return 400 response', async () => {
|
|
834
|
+
const res = await app.request('http://localhost/ping', {
|
|
835
|
+
headers: {
|
|
836
|
+
'x-request-id': 'invalid-key',
|
|
837
|
+
},
|
|
838
|
+
})
|
|
839
|
+
expect(res.status).toBe(400)
|
|
840
|
+
expect(await res.text()).toBe('Invalid!')
|
|
841
|
+
})
|
|
842
|
+
})
|
|
843
|
+
|
|
844
|
+
describe('Validator middleware with Zod validates cookies', () => {
|
|
845
|
+
const app = new App()
|
|
846
|
+
|
|
847
|
+
const schema = z.object({
|
|
848
|
+
debug: z.enum(['0', '1']),
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
app.get('/api/user', zodValidator('cookie', schema), (c) => {
|
|
852
|
+
const { debug } = c.req.valid('cookie')
|
|
853
|
+
return c.json({
|
|
854
|
+
debug,
|
|
855
|
+
})
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
it('Should validate cookies and return 200 response', async () => {
|
|
859
|
+
const res = await app.request('/api/user', {
|
|
860
|
+
headers: {
|
|
861
|
+
Cookie: 'debug=0',
|
|
862
|
+
},
|
|
863
|
+
})
|
|
864
|
+
expect(res.status).toBe(200)
|
|
865
|
+
expect(await res.json()).toEqual({
|
|
866
|
+
debug: '0',
|
|
867
|
+
})
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it('Should validate cookies and return 400 response', async () => {
|
|
871
|
+
const res = await app.request('/api/user', {
|
|
872
|
+
headers: {
|
|
873
|
+
Cookie: 'debug=true',
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
expect(res.status).toBe(400)
|
|
877
|
+
expect(await res.text()).toBe('Invalid!')
|
|
878
|
+
})
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
describe('Validator middleware with Zod multiple validators', () => {
|
|
882
|
+
const app = new App<{ Variables: { id: number } }>()
|
|
883
|
+
const route = app.post(
|
|
884
|
+
'/posts',
|
|
885
|
+
zodValidator(
|
|
886
|
+
'query',
|
|
887
|
+
z.object({
|
|
888
|
+
page: z
|
|
889
|
+
.string()
|
|
890
|
+
.refine((v) => {
|
|
891
|
+
return !isNaN(Number(v))
|
|
892
|
+
})
|
|
893
|
+
.transform((v) => {
|
|
894
|
+
return Number(v)
|
|
895
|
+
}),
|
|
896
|
+
})
|
|
897
|
+
),
|
|
898
|
+
zodValidator(
|
|
899
|
+
'form',
|
|
900
|
+
z.object({
|
|
901
|
+
title: z.string(),
|
|
902
|
+
})
|
|
903
|
+
),
|
|
904
|
+
(c) => {
|
|
905
|
+
const id = c.get('id')
|
|
906
|
+
type verify = Expect<Equal<number, typeof id>>
|
|
907
|
+
const formValidatedData = c.req.valid('form')
|
|
908
|
+
type verify2 = Expect<Equal<{ title: string }, typeof formValidatedData>>
|
|
909
|
+
const { page } = c.req.valid('query')
|
|
910
|
+
const { title } = c.req.valid('form')
|
|
911
|
+
return c.json({ page, title })
|
|
912
|
+
}
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
type Actual = ExtractSchema<typeof route>
|
|
916
|
+
|
|
917
|
+
type Expected = {
|
|
918
|
+
'/posts': {
|
|
919
|
+
$post:
|
|
920
|
+
| {
|
|
921
|
+
input: {
|
|
922
|
+
query: {
|
|
923
|
+
page: string
|
|
924
|
+
}
|
|
925
|
+
} & {
|
|
926
|
+
form: {
|
|
927
|
+
title: string
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
output: 'Invalid!'
|
|
931
|
+
outputFormat: 'text'
|
|
932
|
+
status: 400
|
|
933
|
+
}
|
|
934
|
+
| {
|
|
935
|
+
input: {
|
|
936
|
+
query: {
|
|
937
|
+
page: string
|
|
938
|
+
}
|
|
939
|
+
} & {
|
|
940
|
+
form: {
|
|
941
|
+
title: string
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
output: {
|
|
945
|
+
page: number
|
|
946
|
+
title: string
|
|
947
|
+
}
|
|
948
|
+
outputFormat: 'json'
|
|
949
|
+
status: ContentfulStatusCode
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
955
|
+
|
|
956
|
+
it('Should validate both query param and form data and return 200 response', async () => {
|
|
957
|
+
const form = new FormData()
|
|
958
|
+
form.append('title', 'Hello')
|
|
959
|
+
const res = await app.request('http://localhost/posts?page=2', {
|
|
960
|
+
method: 'POST',
|
|
961
|
+
body: form,
|
|
962
|
+
})
|
|
963
|
+
expect(res.status).toBe(200)
|
|
964
|
+
expect(await res.json()).toEqual({
|
|
965
|
+
page: 2,
|
|
966
|
+
title: 'Hello',
|
|
967
|
+
})
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
it('Should validate both query param and form data and return 400 response', async () => {
|
|
971
|
+
const res = await app.request('http://localhost/posts?page=2', {
|
|
972
|
+
method: 'POST',
|
|
973
|
+
})
|
|
974
|
+
expect(res.status).toBe(400)
|
|
975
|
+
expect(await res.text()).toBe('Invalid!')
|
|
976
|
+
})
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
it('With path parameters', () => {
|
|
980
|
+
const app = new App()
|
|
981
|
+
|
|
982
|
+
const route = app.put(
|
|
983
|
+
'/posts/:id',
|
|
984
|
+
validator('param', () => {
|
|
985
|
+
return {
|
|
986
|
+
id: '123',
|
|
987
|
+
}
|
|
988
|
+
}),
|
|
989
|
+
validator('form', () => {
|
|
990
|
+
return {
|
|
991
|
+
title: 'Foo',
|
|
992
|
+
}
|
|
993
|
+
}),
|
|
994
|
+
(c) => {
|
|
995
|
+
return c.text('Valid!')
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
type Expected = {
|
|
1000
|
+
'/posts/:id': {
|
|
1001
|
+
$put: {
|
|
1002
|
+
input: {
|
|
1003
|
+
form: {
|
|
1004
|
+
title: FormValue | FormValue[]
|
|
1005
|
+
}
|
|
1006
|
+
} & {
|
|
1007
|
+
param: {
|
|
1008
|
+
id: string
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
output: 'Valid!'
|
|
1012
|
+
outputFormat: 'text'
|
|
1013
|
+
status: ContentfulStatusCode
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
type Actual = ExtractSchema<typeof route>
|
|
1019
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
1020
|
+
})
|
|
1021
|
+
|
|
1022
|
+
it('`on`', () => {
|
|
1023
|
+
const app = new App()
|
|
1024
|
+
|
|
1025
|
+
const route = app.on(
|
|
1026
|
+
'PURGE',
|
|
1027
|
+
'/purge',
|
|
1028
|
+
validator('form', () => {
|
|
1029
|
+
return {
|
|
1030
|
+
tag: 'foo',
|
|
1031
|
+
}
|
|
1032
|
+
}),
|
|
1033
|
+
validator('query', () => {
|
|
1034
|
+
return {
|
|
1035
|
+
q: 'bar',
|
|
1036
|
+
}
|
|
1037
|
+
}),
|
|
1038
|
+
(c) => {
|
|
1039
|
+
return c.json({
|
|
1040
|
+
success: true,
|
|
1041
|
+
})
|
|
1042
|
+
}
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
type Expected = {
|
|
1046
|
+
'/purge': {
|
|
1047
|
+
$purge: {
|
|
1048
|
+
input: {
|
|
1049
|
+
form: {
|
|
1050
|
+
tag: FormValue | FormValue[]
|
|
1051
|
+
}
|
|
1052
|
+
} & {
|
|
1053
|
+
query: {
|
|
1054
|
+
q: string | string[]
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
output: {
|
|
1058
|
+
success: true
|
|
1059
|
+
}
|
|
1060
|
+
outputFormat: 'json'
|
|
1061
|
+
status: ContentfulStatusCode
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
type Actual = ExtractSchema<typeof route>
|
|
1067
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
it('`app.on`', () => {
|
|
1071
|
+
const app = new App()
|
|
1072
|
+
|
|
1073
|
+
const route = app
|
|
1074
|
+
.get(
|
|
1075
|
+
'/posts',
|
|
1076
|
+
validator('query', () => {
|
|
1077
|
+
return {
|
|
1078
|
+
page: '2',
|
|
1079
|
+
}
|
|
1080
|
+
}),
|
|
1081
|
+
(c) => {
|
|
1082
|
+
return c.json({
|
|
1083
|
+
posts: [
|
|
1084
|
+
{
|
|
1085
|
+
title: 'foo',
|
|
1086
|
+
},
|
|
1087
|
+
],
|
|
1088
|
+
})
|
|
1089
|
+
}
|
|
1090
|
+
)
|
|
1091
|
+
.post(
|
|
1092
|
+
validator('json', () => {
|
|
1093
|
+
return {
|
|
1094
|
+
title: 'Hello',
|
|
1095
|
+
}
|
|
1096
|
+
}),
|
|
1097
|
+
validator('query', () => {
|
|
1098
|
+
return {
|
|
1099
|
+
title: 'Hello',
|
|
1100
|
+
}
|
|
1101
|
+
}),
|
|
1102
|
+
(c) => {
|
|
1103
|
+
return c.json({
|
|
1104
|
+
success: true,
|
|
1105
|
+
})
|
|
1106
|
+
}
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
type Actual = ExtractSchema<typeof route>
|
|
1110
|
+
//type verify = Expect<Equal<Expected, Actual>>
|
|
1111
|
+
})
|
|
1112
|
+
|
|
1113
|
+
describe('Clone Request object', () => {
|
|
1114
|
+
describe('json', () => {
|
|
1115
|
+
const app = new App()
|
|
1116
|
+
app.post(
|
|
1117
|
+
'/',
|
|
1118
|
+
validator('json', () => {
|
|
1119
|
+
return {
|
|
1120
|
+
foo: 'bar',
|
|
1121
|
+
}
|
|
1122
|
+
}),
|
|
1123
|
+
async (c) => {
|
|
1124
|
+
// `c.req.json()` should not throw the error
|
|
1125
|
+
await c.req.json()
|
|
1126
|
+
// `c.req.text()` should not throw the error
|
|
1127
|
+
await c.req.text()
|
|
1128
|
+
// `c.req.arrayBuffer()` should not throw the error
|
|
1129
|
+
await c.req.arrayBuffer()
|
|
1130
|
+
// `c.req.blob()` should not throw the error
|
|
1131
|
+
await c.req.blob()
|
|
1132
|
+
return c.text('foo')
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
it('Should not throw the error with c.req.json()', async () => {
|
|
1137
|
+
const req = new Request('http://localhost', {
|
|
1138
|
+
method: 'POST',
|
|
1139
|
+
headers: {
|
|
1140
|
+
'Content-Type': 'application/json',
|
|
1141
|
+
},
|
|
1142
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
1143
|
+
})
|
|
1144
|
+
const res = await app.request(req)
|
|
1145
|
+
expect(res.status).toBe(200)
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
|
|
1149
|
+
describe('form', () => {
|
|
1150
|
+
const app = new App()
|
|
1151
|
+
app.post(
|
|
1152
|
+
'/',
|
|
1153
|
+
validator('form', () => {
|
|
1154
|
+
return {
|
|
1155
|
+
foo: 'bar',
|
|
1156
|
+
}
|
|
1157
|
+
}),
|
|
1158
|
+
async (c) => {
|
|
1159
|
+
// `c.req.parseBody()` should not throw the error
|
|
1160
|
+
await c.req.parseBody()
|
|
1161
|
+
// `c.req.text()` should not throw the error
|
|
1162
|
+
await c.req.text()
|
|
1163
|
+
// `c.req.arrayBuffer()` should not throw the error
|
|
1164
|
+
await c.req.arrayBuffer()
|
|
1165
|
+
// `c.req.blob()` should not throw the error
|
|
1166
|
+
await c.req.blob()
|
|
1167
|
+
return c.text('foo')
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
app.post(
|
|
1172
|
+
'/cached',
|
|
1173
|
+
async (c, next) => {
|
|
1174
|
+
await c.req.parseBody()
|
|
1175
|
+
await next()
|
|
1176
|
+
},
|
|
1177
|
+
validator('form', (value) => {
|
|
1178
|
+
if (value instanceof FormData) {
|
|
1179
|
+
throw new Error('The value should not be a FormData')
|
|
1180
|
+
}
|
|
1181
|
+
return value
|
|
1182
|
+
}),
|
|
1183
|
+
(c) => {
|
|
1184
|
+
const v = c.req.valid('form')
|
|
1185
|
+
return c.json(v)
|
|
1186
|
+
}
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
it('Should not throw the error with c.req.parseBody()', async () => {
|
|
1190
|
+
const body = new FormData()
|
|
1191
|
+
body.append('foo', 'bar')
|
|
1192
|
+
const req = new Request('http://localhost', {
|
|
1193
|
+
method: 'POST',
|
|
1194
|
+
body: body,
|
|
1195
|
+
})
|
|
1196
|
+
const res = await app.request(req)
|
|
1197
|
+
expect(res.status).toBe(200)
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
it('Should not be an instance of FormData if the formData is cached', async () => {
|
|
1201
|
+
const body = new FormData()
|
|
1202
|
+
body.append('foo', 'bar')
|
|
1203
|
+
const req = new Request('http://localhost/cached', {
|
|
1204
|
+
method: 'POST',
|
|
1205
|
+
body: body,
|
|
1206
|
+
})
|
|
1207
|
+
const res = await app.request(req)
|
|
1208
|
+
expect(res.status).toBe(200)
|
|
1209
|
+
expect(await res.json()).toEqual({ foo: 'bar' })
|
|
1210
|
+
})
|
|
1211
|
+
})
|
|
1212
|
+
})
|
|
1213
|
+
|
|
1214
|
+
describe('Async validator function', () => {
|
|
1215
|
+
const app = new App()
|
|
1216
|
+
|
|
1217
|
+
app.get(
|
|
1218
|
+
'/posts',
|
|
1219
|
+
validator('query', async () => {
|
|
1220
|
+
return {
|
|
1221
|
+
page: '1',
|
|
1222
|
+
}
|
|
1223
|
+
}),
|
|
1224
|
+
(c) => {
|
|
1225
|
+
const { page } = c.req.valid('query')
|
|
1226
|
+
return c.json({ page })
|
|
1227
|
+
}
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
it('Should get the values from the async function', async () => {
|
|
1231
|
+
const res = await app.request('/posts')
|
|
1232
|
+
expect(res.status).toBe(200)
|
|
1233
|
+
expect(await res.json()).toEqual({
|
|
1234
|
+
page: '1',
|
|
1235
|
+
})
|
|
1236
|
+
})
|
|
1237
|
+
})
|
|
1238
|
+
|
|
1239
|
+
describe('Validator with using Zod directly', () => {
|
|
1240
|
+
it('Should exclude Response & TypedResponse type', () => {
|
|
1241
|
+
const testSchema = z.object({
|
|
1242
|
+
name: z.string(),
|
|
1243
|
+
age: z.number(),
|
|
1244
|
+
type: z.enum(['a']),
|
|
1245
|
+
})
|
|
1246
|
+
const app = new App()
|
|
1247
|
+
|
|
1248
|
+
const route = app.post(
|
|
1249
|
+
'/posts',
|
|
1250
|
+
validator('json', (value, c) => {
|
|
1251
|
+
const parsed = testSchema.safeParse(value)
|
|
1252
|
+
if (!parsed.success) {
|
|
1253
|
+
return c.json({ foo: 'bar' }, 401)
|
|
1254
|
+
}
|
|
1255
|
+
return parsed.data
|
|
1256
|
+
}),
|
|
1257
|
+
(c) => {
|
|
1258
|
+
const data = c.req.valid('json')
|
|
1259
|
+
expectTypeOf(data.name).toEqualTypeOf<string>()
|
|
1260
|
+
return c.json(
|
|
1261
|
+
{
|
|
1262
|
+
message: 'Created!',
|
|
1263
|
+
},
|
|
1264
|
+
201
|
|
1265
|
+
)
|
|
1266
|
+
}
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
expectTypeOf<ExtractSchemaForStatusCode<typeof route, 201>>().toEqualTypeOf<{
|
|
1270
|
+
'/posts': {
|
|
1271
|
+
$post: {
|
|
1272
|
+
input: {
|
|
1273
|
+
json: {
|
|
1274
|
+
type: 'a'
|
|
1275
|
+
name: string
|
|
1276
|
+
age: number
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
output: {
|
|
1280
|
+
message: string
|
|
1281
|
+
}
|
|
1282
|
+
outputFormat: 'json'
|
|
1283
|
+
status: 201
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}>()
|
|
1287
|
+
|
|
1288
|
+
expectTypeOf<ExtractSchemaForStatusCode<typeof route, 401>>().toEqualTypeOf<{
|
|
1289
|
+
'/posts': {
|
|
1290
|
+
$post: {
|
|
1291
|
+
input: {
|
|
1292
|
+
json: {
|
|
1293
|
+
type: 'a'
|
|
1294
|
+
name: string
|
|
1295
|
+
age: number
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
output: {
|
|
1299
|
+
foo: string
|
|
1300
|
+
}
|
|
1301
|
+
outputFormat: 'json'
|
|
1302
|
+
status: 401
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}>()
|
|
1306
|
+
})
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
describe('Transform', () => {
|
|
1310
|
+
it('Should be number when the type is transformed', () => {
|
|
1311
|
+
const route = new App().get(
|
|
1312
|
+
'/',
|
|
1313
|
+
validator('query', async () => {
|
|
1314
|
+
return {
|
|
1315
|
+
page: 1,
|
|
1316
|
+
}
|
|
1317
|
+
}),
|
|
1318
|
+
(c) => {
|
|
1319
|
+
const { page } = c.req.valid('query')
|
|
1320
|
+
expectTypeOf(page).toEqualTypeOf<number>()
|
|
1321
|
+
return c.json({ page })
|
|
1322
|
+
}
|
|
1323
|
+
)
|
|
1324
|
+
|
|
1325
|
+
type Expected = {
|
|
1326
|
+
'/': {
|
|
1327
|
+
$get: {
|
|
1328
|
+
input: {
|
|
1329
|
+
query: {
|
|
1330
|
+
page: string | string[]
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
output: {
|
|
1334
|
+
page: number
|
|
1335
|
+
}
|
|
1336
|
+
outputFormat: 'json'
|
|
1337
|
+
status: ContentfulStatusCode
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
type Actual = ExtractSchema<typeof route>
|
|
1343
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
1344
|
+
|
|
1345
|
+
// Temporary: let's see what the actual type is
|
|
1346
|
+
type TestActual = Actual
|
|
1347
|
+
})
|
|
1348
|
+
|
|
1349
|
+
it('Should be number and union when the type is transformed', () => {
|
|
1350
|
+
const app = new App()
|
|
1351
|
+
const route = app.get(
|
|
1352
|
+
'/',
|
|
1353
|
+
validator('query', () => {
|
|
1354
|
+
return {
|
|
1355
|
+
page: 1,
|
|
1356
|
+
orderBy: 'asc',
|
|
1357
|
+
} as {
|
|
1358
|
+
page: number
|
|
1359
|
+
orderBy: 'asc' | 'desc'
|
|
1360
|
+
orderByWithdefault?: 'asc' | 'desc' | undefined
|
|
1361
|
+
}
|
|
1362
|
+
}),
|
|
1363
|
+
(c) => {
|
|
1364
|
+
const { page, orderBy, orderByWithdefault } = c.req.valid('query')
|
|
1365
|
+
expectTypeOf(page).toEqualTypeOf<number>()
|
|
1366
|
+
expectTypeOf(orderBy).toEqualTypeOf<'asc' | 'desc'>()
|
|
1367
|
+
expectTypeOf(orderByWithdefault).toEqualTypeOf<'asc' | 'desc' | undefined>()
|
|
1368
|
+
return c.json({ page, orderBy, orderByWithdefault })
|
|
1369
|
+
}
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
type Expected = {
|
|
1373
|
+
'/': {
|
|
1374
|
+
$get: {
|
|
1375
|
+
input: {
|
|
1376
|
+
query: {
|
|
1377
|
+
page: string | string[]
|
|
1378
|
+
orderBy: 'asc' | 'desc'
|
|
1379
|
+
orderByWithdefault?: 'asc' | 'desc' | undefined
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
output: {
|
|
1383
|
+
page: number
|
|
1384
|
+
orderBy: 'asc' | 'desc'
|
|
1385
|
+
orderByWithdefault: 'asc' | 'desc' | undefined
|
|
1386
|
+
}
|
|
1387
|
+
outputFormat: 'json'
|
|
1388
|
+
status: ContentfulStatusCode
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
type Actual = ExtractSchema<typeof route>
|
|
1394
|
+
type verify = Expect<Equal<Expected, Actual>>
|
|
1395
|
+
|
|
1396
|
+
// Temporary: let's see what the actual type is
|
|
1397
|
+
type TestActual = Actual
|
|
1398
|
+
})
|
|
1399
|
+
})
|
|
1400
|
+
|
|
1401
|
+
describe('Raw Request cloning after validation', () => {
|
|
1402
|
+
it('Should allow the `cloneRawRequest` util to clone the request object after validation', async () => {
|
|
1403
|
+
const app = new App()
|
|
1404
|
+
|
|
1405
|
+
app.post(
|
|
1406
|
+
'/json-validation',
|
|
1407
|
+
validator('json', (data) => data),
|
|
1408
|
+
async (c) => {
|
|
1409
|
+
const clonedReq = await cloneRawRequest(c.req)
|
|
1410
|
+
const clonedJSON = await clonedReq.json()
|
|
1411
|
+
|
|
1412
|
+
return c.json({
|
|
1413
|
+
originalMethod: c.req.raw.method,
|
|
1414
|
+
clonedMethod: clonedReq.method,
|
|
1415
|
+
clonedUrl: clonedReq.url,
|
|
1416
|
+
clonedHeaders: {
|
|
1417
|
+
contentType: clonedReq.headers.get('Content-Type'),
|
|
1418
|
+
customHeader: clonedReq.headers.get('X-Custom-Header'),
|
|
1419
|
+
},
|
|
1420
|
+
originalCache: c.req.raw.cache,
|
|
1421
|
+
clonedCache: clonedReq.cache,
|
|
1422
|
+
originalCredentials: c.req.raw.credentials,
|
|
1423
|
+
clonedCredentials: clonedReq.credentials,
|
|
1424
|
+
originalMode: c.req.raw.mode,
|
|
1425
|
+
clonedMode: clonedReq.mode,
|
|
1426
|
+
originalRedirect: c.req.raw.redirect,
|
|
1427
|
+
clonedRedirect: clonedReq.redirect,
|
|
1428
|
+
originalReferrerPolicy: c.req.raw.referrerPolicy,
|
|
1429
|
+
clonedReferrerPolicy: clonedReq.referrerPolicy,
|
|
1430
|
+
cloned: JSON.stringify(clonedJSON) === JSON.stringify(await c.req.json()),
|
|
1431
|
+
payload: clonedJSON,
|
|
1432
|
+
})
|
|
1433
|
+
}
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
const testData = { message: 'test', userId: 123 }
|
|
1437
|
+
const res = await app.request('/json-validation', {
|
|
1438
|
+
method: 'POST',
|
|
1439
|
+
headers: {
|
|
1440
|
+
'Content-Type': 'application/json',
|
|
1441
|
+
'X-Custom-Header': 'test-value',
|
|
1442
|
+
},
|
|
1443
|
+
body: JSON.stringify(testData),
|
|
1444
|
+
cache: 'no-cache',
|
|
1445
|
+
credentials: 'include',
|
|
1446
|
+
mode: 'cors',
|
|
1447
|
+
redirect: 'follow',
|
|
1448
|
+
referrerPolicy: 'origin',
|
|
1449
|
+
})
|
|
1450
|
+
|
|
1451
|
+
expect(res.status).toBe(200)
|
|
1452
|
+
|
|
1453
|
+
const result = await res.json()
|
|
1454
|
+
|
|
1455
|
+
expect(result.originalMethod).toBe('POST')
|
|
1456
|
+
expect(result.clonedMethod).toBe('POST')
|
|
1457
|
+
expect(result.clonedUrl).toBe('http://localhost/json-validation')
|
|
1458
|
+
expect(result.clonedHeaders.contentType).toBe('application/json')
|
|
1459
|
+
expect(result.clonedHeaders.customHeader).toBe('test-value')
|
|
1460
|
+
expect(result.clonedCache).toBe(result.originalCache)
|
|
1461
|
+
expect(result.clonedCredentials).toBe(result.originalCredentials)
|
|
1462
|
+
expect(result.clonedMode).toBe(result.originalMode)
|
|
1463
|
+
expect(result.clonedRedirect).toBe(result.originalRedirect)
|
|
1464
|
+
expect(result.clonedReferrerPolicy).toBe(result.originalReferrerPolicy)
|
|
1465
|
+
expect(result.cloned).toBe(true)
|
|
1466
|
+
expect(result.payload).toMatchObject(testData)
|
|
1467
|
+
})
|
|
1468
|
+
})
|
|
1469
|
+
|
|
1470
|
+
describe('Form validator prototype pollution prevention', () => {
|
|
1471
|
+
it('should store __proto__ as data and not misdetect inherited keys', async () => {
|
|
1472
|
+
const app = new App()
|
|
1473
|
+
app.post(
|
|
1474
|
+
'/form',
|
|
1475
|
+
validator('form', (value) => value),
|
|
1476
|
+
(c) => c.json(c.req.valid('form'))
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
const form = new FormData()
|
|
1480
|
+
form.append('__proto__', 'evil')
|
|
1481
|
+
form.append('toString', 'hello')
|
|
1482
|
+
|
|
1483
|
+
const res = await app.request('/form', { method: 'POST', body: form })
|
|
1484
|
+
const result = await res.json()
|
|
1485
|
+
expect(result['__proto__']).toBe('evil')
|
|
1486
|
+
expect(result['toString']).toBe('hello')
|
|
1487
|
+
})
|
|
1488
|
+
})
|