@lovrozagar/honey 0.7.3 → 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +36 -1969
- package/bin/honey.js +87 -27
- package/dist/adapters/bun/serve.d.ts +18 -0
- package/dist/adapters/bun/serve.js +89 -0
- package/dist/adapters/fetch-request.d.ts +20 -0
- package/dist/adapters/fetch-request.js +29 -0
- package/dist/adapters/node/headers.d.ts +27 -0
- package/dist/adapters/node/headers.js +86 -0
- package/dist/adapters/node/request.d.ts +16 -0
- package/dist/adapters/node/request.js +214 -0
- package/dist/adapters/node/response.d.ts +11 -0
- package/dist/adapters/node/response.js +126 -0
- package/dist/adapters/node/serve.d.ts +39 -0
- package/dist/adapters/node/serve.js +205 -0
- package/dist/adapters/node/url.d.ts +13 -0
- package/dist/adapters/node/url.js +87 -0
- package/dist/adapters/serve.d.ts +36 -0
- package/dist/adapters/serve.js +52 -0
- package/dist/build/artifacts.d.ts +34 -0
- package/dist/build/artifacts.js +123 -0
- package/dist/build/cli-args.d.ts +30 -0
- package/dist/build/cli-args.js +97 -0
- package/dist/build/cli.d.ts +11 -0
- package/dist/build/cli.js +150 -0
- package/dist/build/config.d.ts +102 -0
- package/dist/build/config.js +89 -0
- package/dist/build/generate.d.ts +19 -0
- package/dist/build/generate.js +80 -0
- package/dist/build/glob.d.ts +8 -0
- package/dist/build/glob.js +97 -0
- package/dist/build/index.d.ts +3 -58
- package/dist/build/index.js +3 -173
- package/dist/build/init.d.ts +3 -0
- package/dist/build/init.js +131 -0
- package/dist/build/loaders.d.ts +33 -0
- package/dist/build/loaders.js +105 -0
- package/dist/build/plugin.d.ts +52 -0
- package/dist/build/plugin.js +146 -0
- package/dist/build/source.d.ts +21 -0
- package/dist/build/source.js +100 -0
- package/dist/build/targets.d.ts +59 -0
- package/dist/build/targets.js +107 -0
- package/dist/context/context.d.ts +111 -0
- package/dist/context/context.js +213 -0
- package/dist/context/cookie-sign.d.ts +38 -0
- package/dist/context/cookie-sign.js +113 -0
- package/dist/context/cookie.d.ts +28 -0
- package/dist/context/cookie.js +142 -0
- package/dist/context/peer.d.ts +34 -0
- package/dist/context/peer.js +190 -0
- package/dist/errors/error.d.ts +60 -0
- package/dist/errors/error.js +84 -0
- package/dist/errors/errors.d.ts +45 -0
- package/dist/errors/errors.js +31 -0
- package/dist/errors/on-error.d.ts +10 -0
- package/dist/errors/on-error.js +204 -0
- package/dist/errors/problem-json.d.ts +14 -0
- package/dist/errors/problem-json.js +58 -0
- package/dist/errors/projector.d.ts +50 -0
- package/dist/errors/projector.js +107 -0
- package/dist/i18n/error-i18n.d.ts +26 -0
- package/dist/i18n/error-i18n.js +77 -0
- package/dist/i18n/i18n.d.ts +21 -0
- package/dist/i18n/i18n.js +40 -0
- package/dist/i18n/icu.d.ts +29 -0
- package/dist/i18n/icu.js +170 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +3 -0
- package/dist/index.d.ts +30 -746
- package/dist/index.js +12 -2364
- package/dist/input/accepts.d.ts +5 -0
- package/dist/input/accepts.js +105 -0
- package/dist/input/binary.d.ts +20 -0
- package/dist/input/binary.js +40 -0
- package/dist/input/body-kind.d.ts +15 -0
- package/dist/input/body-kind.js +47 -0
- package/dist/input/body-limit.d.ts +21 -0
- package/dist/input/body-limit.js +99 -0
- package/dist/input/cookies.d.ts +7 -0
- package/dist/input/cookies.js +8 -0
- package/dist/input/form.d.ts +10 -0
- package/dist/input/form.js +24 -0
- package/dist/input/headers.d.ts +7 -0
- package/dist/input/headers.js +8 -0
- package/dist/input/input.d.ts +91 -0
- package/dist/input/input.js +88 -0
- package/dist/input/json.d.ts +10 -0
- package/dist/input/json.js +22 -0
- package/dist/input/params.d.ts +7 -0
- package/dist/input/params.js +8 -0
- package/dist/input/query.d.ts +10 -0
- package/dist/input/query.js +17 -0
- package/dist/mcp/mount.d.ts +116 -0
- package/dist/mcp/mount.js +45 -0
- package/dist/mcp/oauth.d.ts +14 -0
- package/dist/mcp/oauth.js +88 -0
- package/dist/mcp/uploads.d.ts +36 -0
- package/dist/mcp/uploads.js +171 -0
- package/dist/mcp/url-files.d.ts +32 -0
- package/dist/mcp/url-files.js +75 -0
- package/dist/meta/meta-spec-apply.d.ts +25 -0
- package/dist/meta/meta-spec-apply.js +83 -0
- package/dist/meta/meta-spec-merge.d.ts +103 -0
- package/dist/meta/meta-spec-merge.js +50 -0
- package/dist/meta/meta-spec-validate.d.ts +61 -0
- package/dist/meta/meta-spec-validate.js +166 -0
- package/dist/meta/meta-spec.d.ts +49 -0
- package/dist/meta/meta-spec.js +197 -0
- package/dist/middleware/compress.d.ts +15 -0
- package/dist/middleware/compress.js +93 -0
- package/dist/middleware/cors.d.ts +18 -0
- package/dist/middleware/cors.js +80 -0
- package/dist/middleware/csrf.d.ts +11 -0
- package/dist/middleware/csrf.js +65 -0
- package/dist/middleware/ip-restrict.d.ts +10 -0
- package/dist/middleware/ip-restrict.js +82 -0
- package/dist/middleware/middleware.d.ts +53 -0
- package/dist/middleware/middleware.js +112 -0
- package/dist/middleware/powered-by.d.ts +6 -0
- package/dist/middleware/powered-by.js +13 -0
- package/dist/middleware/rate-limit.d.ts +40 -0
- package/dist/middleware/rate-limit.js +90 -0
- package/dist/middleware/request-id.d.ts +25 -0
- package/dist/middleware/request-id.js +30 -0
- package/dist/middleware/secure-headers.d.ts +18 -0
- package/dist/middleware/secure-headers.js +36 -0
- package/dist/middleware/server-timing.d.ts +8 -0
- package/dist/middleware/server-timing.js +52 -0
- package/dist/middleware/timeout.d.ts +6 -0
- package/dist/middleware/timeout.js +29 -0
- package/dist/observability/curl-logger.d.ts +46 -0
- package/dist/observability/curl-logger.js +175 -0
- package/dist/observability/request-to-curl.d.ts +17 -0
- package/dist/observability/request-to-curl.js +47 -0
- package/dist/openapi/collect.d.ts +56 -18
- package/dist/openapi/collect.js +109 -23
- package/dist/openapi/components.d.ts +9 -0
- package/dist/openapi/components.js +90 -0
- package/dist/openapi/docs-page.d.ts +20 -18
- package/dist/openapi/docs-page.js +28 -46
- package/dist/openapi/document.d.ts +33 -26
- package/dist/openapi/document.js +21 -780
- package/dist/openapi/extract-schemas.d.ts +11 -0
- package/dist/openapi/extract-schemas.js +31 -0
- package/dist/openapi/generate.d.ts +47 -0
- package/dist/openapi/generate.js +180 -0
- package/dist/openapi/manifest.d.ts +41 -0
- package/dist/openapi/manifest.js +49 -0
- package/dist/openapi/output-keys.d.ts +6 -0
- package/dist/openapi/output-keys.js +24 -0
- package/dist/openapi/paths.d.ts +35 -0
- package/dist/openapi/paths.js +175 -0
- package/dist/openapi/register.d.ts +14 -3
- package/dist/openapi/register.js +13 -20
- package/dist/openapi/responses.d.ts +44 -0
- package/dist/openapi/responses.js +159 -0
- package/dist/openapi/scalar.d.ts +4 -14
- package/dist/openapi/scalar.js +5 -7
- package/dist/openapi/security.d.ts +4 -0
- package/dist/openapi/security.js +25 -0
- package/dist/openapi/serve.d.ts +25 -9
- package/dist/openapi/serve.js +108 -29
- package/dist/openapi/servers.d.ts +19 -0
- package/dist/openapi/servers.js +72 -0
- package/dist/openapi/swagger.d.ts +4 -14
- package/dist/openapi/swagger.js +5 -7
- package/dist/openapi/tags.d.ts +20 -0
- package/dist/openapi/tags.js +20 -0
- package/dist/openapi/webhooks.d.ts +19 -0
- package/dist/openapi/webhooks.js +70 -0
- package/dist/openapi/yaml.d.ts +8 -0
- package/dist/openapi/yaml.js +74 -0
- package/dist/proxy/headers.d.ts +30 -0
- package/dist/proxy/headers.js +86 -0
- package/dist/proxy/proxy.d.ts +39 -0
- package/dist/proxy/proxy.js +103 -0
- package/dist/proxy/upstream.d.ts +30 -0
- package/dist/proxy/upstream.js +116 -0
- package/dist/realtime/bus.d.ts +10 -13
- package/dist/realtime/bus.js +48 -93
- package/dist/realtime/invoke-user.d.ts +36 -0
- package/dist/realtime/invoke-user.js +103 -0
- package/dist/realtime/realtime.d.ts +7 -0
- package/dist/realtime/realtime.js +40 -0
- package/dist/realtime/route.d.ts +33 -27
- package/dist/realtime/route.js +27 -43
- package/dist/realtime/server.d.ts +24 -28
- package/dist/realtime/server.js +202 -225
- package/dist/response/binary.d.ts +18 -0
- package/dist/response/binary.js +10 -0
- package/dist/response/builder.d.ts +73 -0
- package/dist/response/builder.js +214 -0
- package/dist/response/cache-control.d.ts +30 -0
- package/dist/response/cache-control.js +74 -0
- package/dist/response/etag.d.ts +15 -0
- package/dist/response/etag.js +83 -0
- package/dist/response/media-type.d.ts +28 -0
- package/dist/response/media-type.js +97 -0
- package/dist/response/out-headers.d.ts +31 -0
- package/dist/response/out-headers.js +130 -0
- package/dist/response/pretty-json.d.ts +10 -0
- package/dist/response/pretty-json.js +44 -0
- package/dist/response/range.d.ts +19 -0
- package/dist/response/range.js +98 -0
- package/dist/response/response-headers.d.ts +11 -0
- package/dist/response/response-headers.js +21 -0
- package/dist/response/response.d.ts +69 -0
- package/dist/response/response.js +208 -0
- package/dist/response/sse.d.ts +41 -0
- package/dist/response/sse.js +73 -0
- package/dist/response/static.d.ts +20 -0
- package/dist/response/static.js +181 -0
- package/dist/response/status.d.ts +5 -0
- package/dist/response/status.js +68 -0
- package/dist/response/with-headers.d.ts +11 -0
- package/dist/response/with-headers.js +47 -0
- package/dist/routing/app-types.d.ts +181 -0
- package/dist/routing/app-types.js +1 -0
- package/dist/routing/app.d.ts +76 -0
- package/dist/routing/app.js +137 -0
- package/dist/routing/codegen-types.d.ts +17 -0
- package/dist/routing/codegen-types.js +1 -0
- package/dist/routing/delegate.d.ts +9 -0
- package/dist/routing/delegate.js +75 -0
- package/dist/routing/dispatch.d.ts +22 -0
- package/dist/routing/dispatch.js +255 -0
- package/dist/routing/finalize.d.ts +47 -0
- package/dist/routing/finalize.js +169 -0
- package/dist/routing/gateway-tree.d.ts +19 -0
- package/dist/routing/gateway-tree.js +74 -0
- package/dist/routing/invalidate-check.d.ts +58 -0
- package/dist/routing/invalidate-check.js +124 -0
- package/dist/routing/match.d.ts +22 -0
- package/dist/routing/match.js +108 -0
- package/dist/routing/middleware-chain.d.ts +16 -0
- package/dist/routing/middleware-chain.js +41 -0
- package/dist/routing/output-types.d.ts +91 -0
- package/dist/routing/output-types.js +1 -0
- package/dist/routing/pattern.d.ts +55 -0
- package/dist/routing/pattern.js +219 -0
- package/dist/routing/record.d.ts +18 -0
- package/dist/routing/record.js +4 -0
- package/dist/routing/register.d.ts +135 -0
- package/dist/routing/register.js +155 -0
- package/dist/routing/request-path.d.ts +30 -0
- package/dist/routing/request-path.js +173 -0
- package/dist/routing/route-builder.d.ts +160 -0
- package/dist/routing/route-builder.js +86 -0
- package/dist/routing/route-tree.d.ts +18 -0
- package/dist/routing/route-tree.js +91 -0
- package/dist/routing/settings-chain.d.ts +75 -0
- package/dist/routing/settings-chain.js +137 -0
- package/dist/routing/settings.d.ts +90 -0
- package/dist/routing/settings.js +44 -0
- package/dist/routing/tap.d.ts +31 -0
- package/dist/routing/tap.js +77 -0
- package/dist/routing/tree-merge.d.ts +7 -0
- package/dist/routing/tree-merge.js +77 -0
- package/dist/routing/tree.d.ts +85 -0
- package/dist/routing/tree.js +196 -0
- package/dist/routing/types.d.ts +20 -0
- package/dist/routing/types.js +1 -0
- package/dist/routing/ws-builder.d.ts +62 -0
- package/dist/routing/ws-builder.js +109 -0
- package/dist/runtime/detect-runtime.d.ts +16 -0
- package/dist/runtime/detect-runtime.js +11 -0
- package/dist/runtime/optional-peer.d.ts +10 -0
- package/dist/runtime/optional-peer.js +19 -0
- package/dist/schema/arktype.d.ts +7 -0
- package/dist/schema/arktype.js +13 -0
- package/dist/schema/canonicalize.d.ts +8 -0
- package/dist/schema/canonicalize.js +19 -0
- package/dist/schema/effect.d.ts +7 -0
- package/dist/schema/effect.js +4 -0
- package/dist/schema/naming.d.ts +31 -0
- package/dist/schema/naming.js +116 -0
- package/dist/schema/sanitize-spec.d.ts +15 -0
- package/dist/schema/sanitize-spec.js +43 -0
- package/dist/schema/slots.d.ts +15 -0
- package/dist/schema/slots.js +179 -0
- package/dist/schema/to-json-schema.d.ts +34 -0
- package/dist/schema/to-json-schema.js +94 -0
- package/dist/schema/valibot.d.ts +4 -0
- package/dist/schema/valibot.js +153 -0
- package/dist/schema/yup.d.ts +3 -0
- package/dist/schema/yup.js +57 -0
- package/dist/schema/zod.d.ts +6 -0
- package/dist/schema/zod.js +192 -0
- package/dist/security/crypto.d.ts +7 -0
- package/dist/security/crypto.js +30 -0
- package/dist/streaming/producer-stream.d.ts +45 -0
- package/dist/streaming/producer-stream.js +251 -0
- package/dist/telemetry/fields.d.ts +43 -0
- package/dist/telemetry/fields.js +64 -0
- package/dist/telemetry/log-format.d.ts +7 -0
- package/dist/telemetry/log-format.js +43 -0
- package/dist/telemetry/logger.d.ts +44 -0
- package/dist/telemetry/logger.js +122 -0
- package/dist/telemetry/otel-subpath.d.ts +7 -0
- package/dist/telemetry/otel-subpath.js +5 -0
- package/dist/telemetry/otel.d.ts +50 -57
- package/dist/telemetry/otel.js +54 -130
- package/dist/telemetry/request-log.d.ts +22 -0
- package/dist/telemetry/request-log.js +83 -0
- package/dist/telemetry/request-trace.d.ts +123 -0
- package/dist/telemetry/request-trace.js +199 -0
- package/dist/telemetry/span-lifecycle.d.ts +11 -0
- package/dist/telemetry/span-lifecycle.js +114 -0
- package/dist/telemetry/traceparent.d.ts +40 -0
- package/dist/telemetry/traceparent.js +73 -0
- package/dist/testing/testing.d.ts +42 -0
- package/dist/testing/testing.js +101 -0
- package/dist/validation/content-type.d.ts +21 -0
- package/dist/validation/content-type.js +42 -0
- package/dist/validation/standard-schema.d.ts +41 -0
- package/dist/validation/standard-schema.js +5 -0
- package/dist/validation/validate-input.d.ts +45 -0
- package/dist/validation/validate-input.js +174 -0
- package/dist/validation/validate-output.d.ts +34 -0
- package/dist/validation/validate-output.js +107 -0
- package/dist/ws/bun.d.ts +30 -25
- package/dist/ws/bun.js +42 -46
- package/dist/ws/cloudflare.d.ts +6 -75
- package/dist/ws/cloudflare.js +37 -104
- package/dist/ws/contract.d.ts +74 -0
- package/dist/ws/contract.js +83 -0
- package/dist/ws/node.d.ts +12 -21
- package/dist/ws/node.js +103 -151
- package/dist/ws/origin.d.ts +23 -0
- package/dist/ws/origin.js +48 -0
- package/dist/ws/route.d.ts +14 -0
- package/dist/ws/route.js +5 -0
- package/dist/ws/session.d.ts +15 -6
- package/dist/ws/session.js +22 -23
- package/dist/ws/shared.d.ts +23 -33
- package/dist/ws/shared.js +53 -39
- package/package.json +169 -251
- package/dist/accepts.d.ts +0 -12
- package/dist/accepts.d.ts.map +0 -1
- package/dist/accepts.js +0 -126
- package/dist/accepts.js.map +0 -1
- package/dist/body-kind.d.ts +0 -26
- package/dist/body-kind.d.ts.map +0 -1
- package/dist/body-kind.js +0 -70
- package/dist/body-kind.js.map +0 -1
- package/dist/body-limit.d.ts +0 -26
- package/dist/body-limit.d.ts.map +0 -1
- package/dist/body-limit.js +0 -126
- package/dist/body-limit.js.map +0 -1
- package/dist/build/index.d.ts.map +0 -1
- package/dist/build/index.js.map +0 -1
- package/dist/cli-args.d.ts +0 -33
- package/dist/cli-args.d.ts.map +0 -1
- package/dist/cli-args.js +0 -102
- package/dist/cli-args.js.map +0 -1
- package/dist/cli-go/auth.go +0 -26
- package/dist/cli-go/config.go +0 -192
- package/dist/cli-go/errors.go +0 -75
- package/dist/cli-go/multipart.go +0 -79
- package/dist/cli-go/output.go +0 -163
- package/dist/cli-go/root.go +0 -30
- package/dist/cli-go/stream.go +0 -88
- package/dist/cli-go/version.go +0 -5
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -209
- package/dist/cli.js.map +0 -1
- package/dist/client/defaults.d.ts +0 -12
- package/dist/client/defaults.d.ts.map +0 -1
- package/dist/client/defaults.js +0 -48
- package/dist/client/defaults.js.map +0 -1
- package/dist/client/error.d.ts +0 -49
- package/dist/client/error.d.ts.map +0 -1
- package/dist/client/error.js +0 -71
- package/dist/client/error.js.map +0 -1
- package/dist/client/http.d.ts +0 -155
- package/dist/client/http.d.ts.map +0 -1
- package/dist/client/http.js +0 -571
- package/dist/client/http.js.map +0 -1
- package/dist/client/index.d.ts +0 -23
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -95
- package/dist/client/index.js.map +0 -1
- package/dist/client/path.d.ts +0 -26
- package/dist/client/path.d.ts.map +0 -1
- package/dist/client/path.js +0 -163
- package/dist/client/path.js.map +0 -1
- package/dist/client/redirect-policy.d.ts +0 -12
- package/dist/client/redirect-policy.d.ts.map +0 -1
- package/dist/client/redirect-policy.js +0 -20
- package/dist/client/redirect-policy.js.map +0 -1
- package/dist/client/sdk.d.ts +0 -70
- package/dist/client/sdk.d.ts.map +0 -1
- package/dist/client/sdk.js +0 -292
- package/dist/client/sdk.js.map +0 -1
- package/dist/client/sse.d.ts +0 -23
- package/dist/client/sse.d.ts.map +0 -1
- package/dist/client/sse.js +0 -129
- package/dist/client/sse.js.map +0 -1
- package/dist/client/types.d.ts +0 -124
- package/dist/client/types.d.ts.map +0 -1
- package/dist/client/types.js +0 -2
- package/dist/client/types.js.map +0 -1
- package/dist/client/ws.d.ts +0 -22
- package/dist/client/ws.d.ts.map +0 -1
- package/dist/client/ws.js +0 -112
- package/dist/client/ws.js.map +0 -1
- package/dist/client-go/errors.go +0 -236
- package/dist/client-go/export.go +0 -44
- package/dist/client-go/invalidation.go +0 -371
- package/dist/client-go/nullable.go +0 -62
- package/dist/client-go/realtime.go +0 -354
- package/dist/client-go/result.go +0 -20
- package/dist/client-go/runtime.go +0 -975
- package/dist/client-go/sse.go +0 -159
- package/dist/client-go/transport.go +0 -495
- package/dist/client-go/ws.go +0 -128
- package/dist/client-info.d.ts +0 -36
- package/dist/client-info.d.ts.map +0 -1
- package/dist/client-info.js +0 -53
- package/dist/client-info.js.map +0 -1
- package/dist/client-mcp/README.md +0 -10
- package/dist/client-mcp/server.ts +0 -221
- package/dist/client-mcp/types.ts +0 -19
- package/dist/client-python/_errors.py +0 -115
- package/dist/client-python/_invalidation.py +0 -494
- package/dist/client-python/_realtime.py +0 -179
- package/dist/client-python/_runtime.py +0 -860
- package/dist/client-python/_sse.py +0 -155
- package/dist/client-python/_transport.py +0 -366
- package/dist/client-python/_ws.py +0 -159
- package/dist/client-python/py.typed +0 -0
- package/dist/client-rust/errors.rs +0 -220
- package/dist/client-rust/invalidation.rs +0 -412
- package/dist/client-rust/invalidation_sync.rs +0 -93
- package/dist/client-rust/nullable.rs +0 -17
- package/dist/client-rust/realtime.rs +0 -702
- package/dist/client-rust/result.rs +0 -22
- package/dist/client-rust/runtime.rs +0 -980
- package/dist/client-rust/runtime_sync.rs +0 -484
- package/dist/client-rust/sse.rs +0 -188
- package/dist/client-rust/ws.rs +0 -167
- package/dist/codegen-go-cli.d.ts +0 -22
- package/dist/codegen-go-cli.d.ts.map +0 -1
- package/dist/codegen-go-cli.js +0 -964
- package/dist/codegen-go-cli.js.map +0 -1
- package/dist/codegen-go.d.ts +0 -41
- package/dist/codegen-go.d.ts.map +0 -1
- package/dist/codegen-go.js +0 -872
- package/dist/codegen-go.js.map +0 -1
- package/dist/codegen-hash.d.ts +0 -3
- package/dist/codegen-hash.d.ts.map +0 -1
- package/dist/codegen-hash.js +0 -9
- package/dist/codegen-hash.js.map +0 -1
- package/dist/codegen-ir.d.ts +0 -190
- package/dist/codegen-ir.d.ts.map +0 -1
- package/dist/codegen-ir.js +0 -661
- package/dist/codegen-ir.js.map +0 -1
- package/dist/codegen-lang.d.ts +0 -65
- package/dist/codegen-lang.d.ts.map +0 -1
- package/dist/codegen-lang.js +0 -526
- package/dist/codegen-lang.js.map +0 -1
- package/dist/codegen-load.d.ts +0 -31
- package/dist/codegen-load.d.ts.map +0 -1
- package/dist/codegen-load.js +0 -138
- package/dist/codegen-load.js.map +0 -1
- package/dist/codegen-loaders.d.ts +0 -12
- package/dist/codegen-loaders.d.ts.map +0 -1
- package/dist/codegen-loaders.js +0 -55
- package/dist/codegen-loaders.js.map +0 -1
- package/dist/codegen-mcp.d.ts +0 -13
- package/dist/codegen-mcp.d.ts.map +0 -1
- package/dist/codegen-mcp.js +0 -368
- package/dist/codegen-mcp.js.map +0 -1
- package/dist/codegen-python.d.ts +0 -33
- package/dist/codegen-python.d.ts.map +0 -1
- package/dist/codegen-python.js +0 -828
- package/dist/codegen-python.js.map +0 -1
- package/dist/codegen-route-tree-intern.d.ts +0 -40
- package/dist/codegen-route-tree-intern.d.ts.map +0 -1
- package/dist/codegen-route-tree-intern.js +0 -221
- package/dist/codegen-route-tree-intern.js.map +0 -1
- package/dist/codegen-rust.d.ts +0 -18
- package/dist/codegen-rust.d.ts.map +0 -1
- package/dist/codegen-rust.js +0 -1293
- package/dist/codegen-rust.js.map +0 -1
- package/dist/codegen-sanitize.d.ts +0 -3
- package/dist/codegen-sanitize.d.ts.map +0 -1
- package/dist/codegen-sanitize.js +0 -56
- package/dist/codegen-sanitize.js.map +0 -1
- package/dist/codegen-schema-naming.d.ts +0 -27
- package/dist/codegen-schema-naming.d.ts.map +0 -1
- package/dist/codegen-schema-naming.js +0 -150
- package/dist/codegen-schema-naming.js.map +0 -1
- package/dist/codegen-sdk-model.d.ts +0 -71
- package/dist/codegen-sdk-model.d.ts.map +0 -1
- package/dist/codegen-sdk-model.js +0 -179
- package/dist/codegen-sdk-model.js.map +0 -1
- package/dist/codegen.d.ts +0 -187
- package/dist/codegen.d.ts.map +0 -1
- package/dist/codegen.js +0 -4619
- package/dist/codegen.js.map +0 -1
- package/dist/context.d.ts +0 -86
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -242
- package/dist/context.js.map +0 -1
- package/dist/cookie-sign.d.ts +0 -17
- package/dist/cookie-sign.d.ts.map +0 -1
- package/dist/cookie-sign.js +0 -104
- package/dist/cookie-sign.js.map +0 -1
- package/dist/cookie.d.ts +0 -24
- package/dist/cookie.d.ts.map +0 -1
- package/dist/cookie.js +0 -138
- package/dist/cookie.js.map +0 -1
- package/dist/cors.d.ts +0 -14
- package/dist/cors.d.ts.map +0 -1
- package/dist/cors.js +0 -87
- package/dist/cors.js.map +0 -1
- package/dist/crypto.d.ts +0 -7
- package/dist/crypto.d.ts.map +0 -1
- package/dist/crypto.js +0 -38
- package/dist/crypto.js.map +0 -1
- package/dist/csrf.d.ts +0 -28
- package/dist/csrf.d.ts.map +0 -1
- package/dist/csrf.js +0 -79
- package/dist/csrf.js.map +0 -1
- package/dist/curl-logger.d.ts +0 -44
- package/dist/curl-logger.d.ts.map +0 -1
- package/dist/curl-logger.js +0 -199
- package/dist/curl-logger.js.map +0 -1
- package/dist/detect-runtime.d.ts +0 -16
- package/dist/detect-runtime.d.ts.map +0 -1
- package/dist/detect-runtime.js +0 -13
- package/dist/detect-runtime.js.map +0 -1
- package/dist/dict.d.ts +0 -11
- package/dist/dict.d.ts.map +0 -1
- package/dist/dict.js +0 -13
- package/dist/dict.js.map +0 -1
- package/dist/error.d.ts +0 -21
- package/dist/error.d.ts.map +0 -1
- package/dist/error.js +0 -44
- package/dist/error.js.map +0 -1
- package/dist/errors.d.ts +0 -40
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -39
- package/dist/errors.js.map +0 -1
- package/dist/etag.d.ts +0 -11
- package/dist/etag.d.ts.map +0 -1
- package/dist/etag.js +0 -82
- package/dist/etag.js.map +0 -1
- package/dist/feature-detect.d.ts +0 -25
- package/dist/feature-detect.d.ts.map +0 -1
- package/dist/feature-detect.js +0 -144
- package/dist/feature-detect.js.map +0 -1
- package/dist/feature-load.d.ts +0 -3
- package/dist/feature-load.d.ts.map +0 -1
- package/dist/feature-load.js +0 -36
- package/dist/feature-load.js.map +0 -1
- package/dist/feature-slots.d.ts +0 -44
- package/dist/feature-slots.d.ts.map +0 -1
- package/dist/feature-slots.js +0 -48
- package/dist/feature-slots.js.map +0 -1
- package/dist/fetch-request.d.ts +0 -10
- package/dist/fetch-request.d.ts.map +0 -1
- package/dist/fetch-request.js +0 -13
- package/dist/fetch-request.js.map +0 -1
- package/dist/gen-process.d.ts +0 -15
- package/dist/gen-process.d.ts.map +0 -1
- package/dist/gen-process.js +0 -43
- package/dist/gen-process.js.map +0 -1
- package/dist/gen-write.d.ts +0 -19
- package/dist/gen-write.d.ts.map +0 -1
- package/dist/gen-write.js +0 -120
- package/dist/gen-write.js.map +0 -1
- package/dist/glob.d.ts +0 -21
- package/dist/glob.d.ts.map +0 -1
- package/dist/glob.js +0 -115
- package/dist/glob.js.map +0 -1
- package/dist/go-type-emitter.d.ts +0 -47
- package/dist/go-type-emitter.d.ts.map +0 -1
- package/dist/go-type-emitter.js +0 -594
- package/dist/go-type-emitter.js.map +0 -1
- package/dist/honey-response.d.ts +0 -82
- package/dist/honey-response.d.ts.map +0 -1
- package/dist/honey-response.js +0 -338
- package/dist/honey-response.js.map +0 -1
- package/dist/i18n-slot.d.ts +0 -15
- package/dist/i18n-slot.d.ts.map +0 -1
- package/dist/i18n-slot.js +0 -14
- package/dist/i18n-slot.js.map +0 -1
- package/dist/i18n.d.ts +0 -29
- package/dist/i18n.d.ts.map +0 -1
- package/dist/i18n.js +0 -250
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/init.d.ts +0 -5
- package/dist/init.d.ts.map +0 -1
- package/dist/init.js +0 -140
- package/dist/init.js.map +0 -1
- package/dist/input.d.ts +0 -6
- package/dist/input.d.ts.map +0 -1
- package/dist/input.js +0 -4
- package/dist/input.js.map +0 -1
- package/dist/invalidate-check.d.ts +0 -61
- package/dist/invalidate-check.d.ts.map +0 -1
- package/dist/invalidate-check.js +0 -163
- package/dist/invalidate-check.js.map +0 -1
- package/dist/invoke-user.d.ts +0 -35
- package/dist/invoke-user.d.ts.map +0 -1
- package/dist/invoke-user.js +0 -73
- package/dist/invoke-user.js.map +0 -1
- package/dist/ip-restrict.d.ts +0 -24
- package/dist/ip-restrict.d.ts.map +0 -1
- package/dist/ip-restrict.js +0 -60
- package/dist/ip-restrict.js.map +0 -1
- package/dist/ip.d.ts +0 -39
- package/dist/ip.d.ts.map +0 -1
- package/dist/ip.js +0 -209
- package/dist/ip.js.map +0 -1
- package/dist/logger.d.ts +0 -45
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -113
- package/dist/logger.js.map +0 -1
- package/dist/media-type.d.ts +0 -30
- package/dist/media-type.d.ts.map +0 -1
- package/dist/media-type.js +0 -97
- package/dist/media-type.js.map +0 -1
- package/dist/meta-spec-merge.d.ts +0 -32
- package/dist/meta-spec-merge.d.ts.map +0 -1
- package/dist/meta-spec-merge.js +0 -68
- package/dist/meta-spec-merge.js.map +0 -1
- package/dist/meta-spec.d.ts +0 -94
- package/dist/meta-spec.d.ts.map +0 -1
- package/dist/meta-spec.js +0 -535
- package/dist/meta-spec.js.map +0 -1
- package/dist/middleware.d.ts +0 -77
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -253
- package/dist/middleware.js.map +0 -1
- package/dist/node-request.d.ts +0 -104
- package/dist/node-request.d.ts.map +0 -1
- package/dist/node-request.js +0 -407
- package/dist/node-request.js.map +0 -1
- package/dist/node.d.ts +0 -30
- package/dist/node.d.ts.map +0 -1
- package/dist/node.js +0 -396
- package/dist/node.js.map +0 -1
- package/dist/openapi/collect.d.ts.map +0 -1
- package/dist/openapi/collect.js.map +0 -1
- package/dist/openapi/docs-page.d.ts.map +0 -1
- package/dist/openapi/docs-page.js.map +0 -1
- package/dist/openapi/document.d.ts.map +0 -1
- package/dist/openapi/document.js.map +0 -1
- package/dist/openapi/json-schema-slot.d.ts +0 -12
- package/dist/openapi/json-schema-slot.d.ts.map +0 -1
- package/dist/openapi/json-schema-slot.js +0 -38
- package/dist/openapi/json-schema-slot.js.map +0 -1
- package/dist/openapi/register.d.ts.map +0 -1
- package/dist/openapi/register.js.map +0 -1
- package/dist/openapi/scalar.d.ts.map +0 -1
- package/dist/openapi/scalar.js.map +0 -1
- package/dist/openapi/serve.d.ts.map +0 -1
- package/dist/openapi/serve.js.map +0 -1
- package/dist/openapi/spec-factory.d.ts +0 -65
- package/dist/openapi/spec-factory.d.ts.map +0 -1
- package/dist/openapi/spec-factory.js +0 -52
- package/dist/openapi/spec-factory.js.map +0 -1
- package/dist/openapi/spec.d.ts +0 -29
- package/dist/openapi/spec.d.ts.map +0 -1
- package/dist/openapi/spec.js +0 -50
- package/dist/openapi/spec.js.map +0 -1
- package/dist/openapi/swagger.d.ts.map +0 -1
- package/dist/openapi/swagger.js.map +0 -1
- package/dist/pattern.d.ts +0 -71
- package/dist/pattern.d.ts.map +0 -1
- package/dist/pattern.js +0 -239
- package/dist/pattern.js.map +0 -1
- package/dist/peer.d.ts +0 -17
- package/dist/peer.d.ts.map +0 -1
- package/dist/peer.js +0 -50
- package/dist/peer.js.map +0 -1
- package/dist/plugin.d.ts +0 -228
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -666
- package/dist/plugin.js.map +0 -1
- package/dist/powered-by.d.ts +0 -7
- package/dist/powered-by.d.ts.map +0 -1
- package/dist/powered-by.js +0 -11
- package/dist/powered-by.js.map +0 -1
- package/dist/pretty-json.d.ts +0 -10
- package/dist/pretty-json.d.ts.map +0 -1
- package/dist/pretty-json.js +0 -47
- package/dist/pretty-json.js.map +0 -1
- package/dist/producer-stream.d.ts +0 -47
- package/dist/producer-stream.d.ts.map +0 -1
- package/dist/producer-stream.js +0 -267
- package/dist/producer-stream.js.map +0 -1
- package/dist/proxy.d.ts +0 -90
- package/dist/proxy.d.ts.map +0 -1
- package/dist/proxy.js +0 -274
- package/dist/proxy.js.map +0 -1
- package/dist/python-type-emitter.d.ts +0 -24
- package/dist/python-type-emitter.d.ts.map +0 -1
- package/dist/python-type-emitter.js +0 -157
- package/dist/python-type-emitter.js.map +0 -1
- package/dist/realtime/bus.d.ts.map +0 -1
- package/dist/realtime/bus.js.map +0 -1
- package/dist/realtime/register.d.ts +0 -3
- package/dist/realtime/register.d.ts.map +0 -1
- package/dist/realtime/register.js +0 -10
- package/dist/realtime/register.js.map +0 -1
- package/dist/realtime/route.d.ts.map +0 -1
- package/dist/realtime/route.js.map +0 -1
- package/dist/realtime/server.d.ts.map +0 -1
- package/dist/realtime/server.js.map +0 -1
- package/dist/request-id.d.ts +0 -19
- package/dist/request-id.d.ts.map +0 -1
- package/dist/request-id.js +0 -16
- package/dist/request-id.js.map +0 -1
- package/dist/request-limits.d.ts +0 -3
- package/dist/request-limits.d.ts.map +0 -1
- package/dist/request-limits.js +0 -3
- package/dist/request-limits.js.map +0 -1
- package/dist/request-path.d.ts +0 -33
- package/dist/request-path.d.ts.map +0 -1
- package/dist/request-path.js +0 -178
- package/dist/request-path.js.map +0 -1
- package/dist/request-to-curl.d.ts +0 -15
- package/dist/request-to-curl.d.ts.map +0 -1
- package/dist/request-to-curl.js +0 -48
- package/dist/request-to-curl.js.map +0 -1
- package/dist/response.d.ts +0 -86
- package/dist/response.d.ts.map +0 -1
- package/dist/response.js +0 -313
- package/dist/response.js.map +0 -1
- package/dist/rust-type-emitter.d.ts +0 -44
- package/dist/rust-type-emitter.d.ts.map +0 -1
- package/dist/rust-type-emitter.js +0 -463
- package/dist/rust-type-emitter.js.map +0 -1
- package/dist/secure-headers.d.ts +0 -16
- package/dist/secure-headers.d.ts.map +0 -1
- package/dist/secure-headers.js +0 -53
- package/dist/secure-headers.js.map +0 -1
- package/dist/serve-register.d.ts +0 -6
- package/dist/serve-register.d.ts.map +0 -1
- package/dist/serve-register.js +0 -9
- package/dist/serve-register.js.map +0 -1
- package/dist/serve-slot.d.ts +0 -9
- package/dist/serve-slot.d.ts.map +0 -1
- package/dist/serve-slot.js +0 -52
- package/dist/serve-slot.js.map +0 -1
- package/dist/serve.d.ts +0 -38
- package/dist/serve.d.ts.map +0 -1
- package/dist/serve.js +0 -157
- package/dist/serve.js.map +0 -1
- package/dist/server-timing.d.ts +0 -9
- package/dist/server-timing.d.ts.map +0 -1
- package/dist/server-timing.js +0 -51
- package/dist/server-timing.js.map +0 -1
- package/dist/static.d.ts +0 -21
- package/dist/static.d.ts.map +0 -1
- package/dist/static.js +0 -102
- package/dist/static.js.map +0 -1
- package/dist/telemetry/otel.d.ts.map +0 -1
- package/dist/telemetry/otel.js.map +0 -1
- package/dist/testing.d.ts +0 -36
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -124
- package/dist/testing.js.map +0 -1
- package/dist/timeout.d.ts +0 -13
- package/dist/timeout.d.ts.map +0 -1
- package/dist/timeout.js +0 -36
- package/dist/timeout.js.map +0 -1
- package/dist/tree.d.ts +0 -197
- package/dist/tree.d.ts.map +0 -1
- package/dist/tree.js +0 -437
- package/dist/tree.js.map +0 -1
- package/dist/trust.d.ts +0 -21
- package/dist/trust.d.ts.map +0 -1
- package/dist/trust.js +0 -100
- package/dist/trust.js.map +0 -1
- package/dist/ts-type-emitter.d.ts +0 -8
- package/dist/ts-type-emitter.d.ts.map +0 -1
- package/dist/ts-type-emitter.js +0 -150
- package/dist/ts-type-emitter.js.map +0 -1
- package/dist/type-emitter.d.ts +0 -36
- package/dist/type-emitter.d.ts.map +0 -1
- package/dist/type-emitter.js +0 -729
- package/dist/type-emitter.js.map +0 -1
- package/dist/type-extractor.d.ts +0 -43
- package/dist/type-extractor.d.ts.map +0 -1
- package/dist/type-extractor.js +0 -819
- package/dist/type-extractor.js.map +0 -1
- package/dist/types.d.ts +0 -467
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -96
- package/dist/types.js.map +0 -1
- package/dist/validation.d.ts +0 -31
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -335
- package/dist/validation.js.map +0 -1
- package/dist/with-headers.d.ts +0 -15
- package/dist/with-headers.d.ts.map +0 -1
- package/dist/with-headers.js +0 -45
- package/dist/with-headers.js.map +0 -1
- package/dist/ws/bun.d.ts.map +0 -1
- package/dist/ws/bun.js.map +0 -1
- package/dist/ws/cloudflare.d.ts.map +0 -1
- package/dist/ws/cloudflare.js.map +0 -1
- package/dist/ws/deno.d.ts +0 -13
- package/dist/ws/deno.d.ts.map +0 -1
- package/dist/ws/deno.js +0 -133
- package/dist/ws/deno.js.map +0 -1
- package/dist/ws/node.d.ts.map +0 -1
- package/dist/ws/node.js.map +0 -1
- package/dist/ws/session.d.ts.map +0 -1
- package/dist/ws/session.js.map +0 -1
- package/dist/ws/shared.d.ts.map +0 -1
- package/dist/ws/shared.js.map +0 -1
- package/dist/ws-origin.d.ts +0 -20
- package/dist/ws-origin.d.ts.map +0 -1
- package/dist/ws-origin.js +0 -41
- package/dist/ws-origin.js.map +0 -1
- package/dist/yaml.d.ts +0 -18
- package/dist/yaml.d.ts.map +0 -1
- package/dist/yaml.js +0 -101
- package/dist/yaml.js.map +0 -1
- package/docs/api-ref/go.md +0 -399
- package/docs/api-ref/python.md +0 -399
- package/docs/api-ref/rust.md +0 -399
- package/docs/api-ref/typescript.md +0 -399
- package/docs/invalidate-spec.md +0 -333
- package/docs/meta-spec.md +0 -555
- package/docs/packaging.md +0 -104
- package/docs/regression-matrix/README.md +0 -32
- package/docs/regression-matrix/review.md +0 -44
- package/docs/regression-matrix/ws1-3.md +0 -97
- package/docs/regression-matrix/ws4-8.md +0 -107
- package/docs/regression-matrix/ws9-12.md +0 -266
- package/docs/sdk.md +0 -798
- package/src/accepts.ts +0 -136
- package/src/body-kind.ts +0 -67
- package/src/body-limit.ts +0 -152
- package/src/build/index.ts +0 -217
- package/src/cli-args.ts +0 -123
- package/src/cli-go/auth.go +0 -26
- package/src/cli-go/config.go +0 -192
- package/src/cli-go/errors.go +0 -75
- package/src/cli-go/multipart.go +0 -79
- package/src/cli-go/output.go +0 -163
- package/src/cli-go/root.go +0 -30
- package/src/cli-go/stream.go +0 -88
- package/src/cli-go/version.go +0 -5
- package/src/cli.ts +0 -209
- package/src/client/defaults.ts +0 -47
- package/src/client/error.ts +0 -82
- package/src/client/http.ts +0 -737
- package/src/client/index.ts +0 -157
- package/src/client/path.ts +0 -169
- package/src/client/redirect-policy.ts +0 -19
- package/src/client/sdk.ts +0 -351
- package/src/client/sse.ts +0 -128
- package/src/client/types.ts +0 -206
- package/src/client/ws.ts +0 -149
- package/src/client-go/errors.go +0 -236
- package/src/client-go/export.go +0 -44
- package/src/client-go/invalidation.go +0 -371
- package/src/client-go/nullable.go +0 -62
- package/src/client-go/realtime.go +0 -354
- package/src/client-go/result.go +0 -20
- package/src/client-go/runtime.go +0 -975
- package/src/client-go/sse.go +0 -159
- package/src/client-go/transport.go +0 -495
- package/src/client-go/ws.go +0 -128
- package/src/client-info.ts +0 -79
- package/src/client-mcp/README.md +0 -10
- package/src/client-mcp/server.ts +0 -221
- package/src/client-mcp/types.ts +0 -19
- package/src/client-python/_errors.py +0 -115
- package/src/client-python/_invalidation.py +0 -494
- package/src/client-python/_realtime.py +0 -179
- package/src/client-python/_runtime.py +0 -860
- package/src/client-python/_sse.py +0 -155
- package/src/client-python/_transport.py +0 -366
- package/src/client-python/_ws.py +0 -159
- package/src/client-python/py.typed +0 -0
- package/src/client-rust/errors.rs +0 -220
- package/src/client-rust/invalidation.rs +0 -412
- package/src/client-rust/invalidation_sync.rs +0 -93
- package/src/client-rust/nullable.rs +0 -17
- package/src/client-rust/realtime.rs +0 -702
- package/src/client-rust/result.rs +0 -22
- package/src/client-rust/runtime.rs +0 -980
- package/src/client-rust/runtime_sync.rs +0 -484
- package/src/client-rust/sse.rs +0 -188
- package/src/client-rust/ws.rs +0 -167
- package/src/codegen-go-cli.ts +0 -1066
- package/src/codegen-go.ts +0 -970
- package/src/codegen-hash.ts +0 -8
- package/src/codegen-ir.ts +0 -812
- package/src/codegen-lang.ts +0 -517
- package/src/codegen-load.ts +0 -150
- package/src/codegen-loaders.ts +0 -50
- package/src/codegen-mcp.ts +0 -445
- package/src/codegen-python.ts +0 -916
- package/src/codegen-route-tree-intern.ts +0 -198
- package/src/codegen-rust.ts +0 -1402
- package/src/codegen-sanitize.ts +0 -57
- package/src/codegen-schema-naming.ts +0 -165
- package/src/codegen-sdk-model.ts +0 -228
- package/src/codegen.ts +0 -5089
- package/src/context.ts +0 -325
- package/src/cookie-sign.ts +0 -119
- package/src/cookie.ts +0 -143
- package/src/cors.ts +0 -95
- package/src/crypto.ts +0 -38
- package/src/csrf.ts +0 -86
- package/src/curl-logger.ts +0 -258
- package/src/detect-runtime.ts +0 -17
- package/src/dict.ts +0 -12
- package/src/error.ts +0 -55
- package/src/errors.ts +0 -85
- package/src/etag.ts +0 -97
- package/src/feature-detect.ts +0 -147
- package/src/feature-load.ts +0 -23
- package/src/feature-slots.ts +0 -70
- package/src/fetch-request.ts +0 -13
- package/src/gen-process.ts +0 -42
- package/src/gen-write.ts +0 -127
- package/src/glob.ts +0 -109
- package/src/go-type-emitter.ts +0 -660
- package/src/honey-response.ts +0 -379
- package/src/i18n-slot.ts +0 -30
- package/src/i18n.ts +0 -300
- package/src/index.ts +0 -3829
- package/src/init.ts +0 -161
- package/src/input.ts +0 -8
- package/src/invalidate-check.ts +0 -205
- package/src/invoke-user.ts +0 -95
- package/src/ip-restrict.ts +0 -79
- package/src/ip.ts +0 -194
- package/src/logger.ts +0 -178
- package/src/media-type.ts +0 -104
- package/src/meta-spec-merge.ts +0 -80
- package/src/meta-spec.ts +0 -718
- package/src/middleware.ts +0 -331
- package/src/node-request.ts +0 -431
- package/src/node.ts +0 -454
- package/src/openapi/collect.ts +0 -50
- package/src/openapi/docs-page.ts +0 -89
- package/src/openapi/document.ts +0 -902
- package/src/openapi/json-schema-slot.ts +0 -53
- package/src/openapi/register.ts +0 -23
- package/src/openapi/scalar.ts +0 -34
- package/src/openapi/serve.ts +0 -39
- package/src/openapi/spec-factory.ts +0 -100
- package/src/openapi/spec.ts +0 -74
- package/src/openapi/swagger.ts +0 -35
- package/src/pattern.ts +0 -245
- package/src/peer.ts +0 -53
- package/src/plugin.ts +0 -864
- package/src/powered-by.ts +0 -18
- package/src/pretty-json.ts +0 -56
- package/src/producer-stream.ts +0 -296
- package/src/proxy.ts +0 -370
- package/src/python-type-emitter.ts +0 -181
- package/src/realtime/bus.ts +0 -140
- package/src/realtime/register.ts +0 -13
- package/src/realtime/route.ts +0 -153
- package/src/realtime/server.ts +0 -298
- package/src/request-id.ts +0 -32
- package/src/request-limits.ts +0 -2
- package/src/request-path.ts +0 -165
- package/src/request-to-curl.ts +0 -52
- package/src/response.ts +0 -411
- package/src/rust-type-emitter.ts +0 -548
- package/src/secure-headers.ts +0 -74
- package/src/serve-register.ts +0 -12
- package/src/serve-slot.ts +0 -58
- package/src/serve.ts +0 -220
- package/src/server-timing.ts +0 -71
- package/src/static.ts +0 -112
- package/src/telemetry/otel.ts +0 -156
- package/src/testing.ts +0 -167
- package/src/timeout.ts +0 -45
- package/src/tree.ts +0 -558
- package/src/trust.ts +0 -117
- package/src/ts-type-emitter.ts +0 -168
- package/src/type-emitter.ts +0 -760
- package/src/type-extractor.ts +0 -855
- package/src/types.ts +0 -756
- package/src/validation.ts +0 -413
- package/src/with-headers.ts +0 -45
- package/src/ws/bun.ts +0 -120
- package/src/ws/cloudflare.ts +0 -192
- package/src/ws/deno.ts +0 -188
- package/src/ws/node.ts +0 -240
- package/src/ws/session.ts +0 -73
- package/src/ws/shared.ts +0 -140
- package/src/ws-origin.ts +0 -53
- package/src/yaml.ts +0 -103
package/README.md
CHANGED
|
@@ -1,1999 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lovrozagar/honey
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
This README is the full usage manual. An agent that reads only this file should be able to build, serve, generate, and consume a Honey app without opening source.
|
|
8
|
-
|
|
9
|
-
## Table of contents
|
|
10
|
-
|
|
11
|
-
- [Start](#start)
|
|
12
|
-
- [What Honey is](#what-honey-is)
|
|
13
|
-
- [Install](#install)
|
|
14
|
-
- [First app](#first-app)
|
|
15
|
-
- [CLI](#cli)
|
|
16
|
-
- [`honey init`](#honey-init)
|
|
17
|
-
- [`honey generate`](#honey-generate)
|
|
18
|
-
- [Builder](#builder)
|
|
19
|
-
- [Create the app](#create-the-app)
|
|
20
|
-
- [Routes and methods](#routes-and-methods)
|
|
21
|
-
- [Path matching](#path-matching)
|
|
22
|
-
- [Prefixes and slashes](#prefixes-and-slashes)
|
|
23
|
-
- [Context](#context)
|
|
24
|
-
- [Responses](#responses)
|
|
25
|
-
- [Status keys](#status-keys)
|
|
26
|
-
- [Input](#input)
|
|
27
|
-
- [Output](#output)
|
|
28
|
-
- [Errors](#errors)
|
|
29
|
-
- [Middleware](#middleware)
|
|
30
|
-
- [Shipped middleware](#shipped-middleware)
|
|
31
|
-
- [Composition](#composition)
|
|
32
|
-
- [Taps](#taps)
|
|
33
|
-
- [Proxy routes](#proxy-routes)
|
|
34
|
-
- [Static files](#static-files)
|
|
35
|
-
- [Logging, telemetry, production tree](#logging-telemetry-production-tree)
|
|
36
|
-
- [Serve](#serve)
|
|
37
|
-
- [Bun, Node, Deno](#bun-node-deno)
|
|
38
|
-
- [Cloudflare Workers](#cloudflare-workers)
|
|
39
|
-
- [Feature auto-load](#feature-auto-load)
|
|
40
|
-
- [OpenAPI, docs, and manifest](#openapi-docs-and-manifest)
|
|
41
|
-
- [Meta spec](#meta-spec)
|
|
42
|
-
- [WebSockets](#websockets)
|
|
43
|
-
- [Realtime](#realtime)
|
|
44
|
-
- [SSE and streaming](#sse-and-streaming)
|
|
45
|
-
- [Generated clients](#generated-clients)
|
|
46
|
-
- [Plugin codegen config](#plugin-codegen-config)
|
|
47
|
-
- [TypeScript `createClient`](#typescript-createclient)
|
|
48
|
-
- [Generated SDK usage](#generated-sdk-usage)
|
|
49
|
-
- [Go CLI](#go-cli)
|
|
50
|
-
- [Programmatic codegen](#programmatic-codegen)
|
|
51
|
-
- [Type inference](#type-inference)
|
|
52
|
-
- [Testing](#testing)
|
|
53
|
-
- [Utilities](#utilities)
|
|
54
|
-
- [Package exports](#package-exports)
|
|
55
|
-
- [Repository layout](#repository-layout)
|
|
56
|
-
- [Develop](#develop)
|
|
57
|
-
- [Test matrix](#test-matrix)
|
|
58
|
-
- [E2E apps and runtimes](#e2e-apps-and-runtimes)
|
|
59
|
-
- [Live Cloudflare proof](#live-cloudflare-proof)
|
|
60
|
-
- [Bench](#bench)
|
|
61
|
-
- [Releases](#releases)
|
|
62
|
-
- [License](#license)
|
|
63
|
-
|
|
64
|
-
## Start
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
bun add @lovrozagar/honey
|
|
68
|
-
honey init
|
|
69
|
-
bun run dev
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
import { honey } from "@lovrozagar/honey"
|
|
74
|
-
|
|
75
|
-
export const app = honey()
|
|
76
|
-
.get("/health")
|
|
77
|
-
.handler((ctx) => ctx.res.text("ok", "ok"))
|
|
78
|
-
.openapi({ docs: "scalar", title: "My API", version: "1.0.0" })
|
|
79
|
-
|
|
80
|
-
await app.serve({ cors: true, port: 3000 })
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
curl http://127.0.0.1:3000/health
|
|
85
|
-
# ok
|
|
86
|
-
|
|
87
|
-
curl http://127.0.0.1:3000/openapi.json
|
|
88
|
-
# open http://127.0.0.1:3000/docs
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
`honey init` writes `src/app.ts`, `src/server.ts`, `vite.config.ts`, and `dev` / `generate` scripts. `honey init --cf` also writes `wrangler.jsonc` and a worker that exports `fetch`.
|
|
92
|
-
|
|
93
|
-
`honey generate` loads the app in a fresh process with `app.serve()` stubbed, so the one-file shape above does not start a listener while generating, and the process exits when it is done. Other top-level side effects in the app module (database clients, timers) still run during generation; keep them behind a function or in `src/server.ts`.
|
|
94
|
-
|
|
95
|
-
`app.serve()` detects bun / node / deno and loads only that WebSocket adapter. Pass `runtime` to pin it. Cloudflare Workers cannot listen — export `fetch: (req, env, ctx) => app.fetch(req, env, ctx)`.
|
|
96
|
-
|
|
97
|
-
`app.serve()`, `app.openapi()`, and `app.errorI18n()` load their implementations when called. A fetch-only production bundle does not include listen adapters, spec generation, or i18n. Spec and docs routes stay out of the generated document.
|
|
98
|
-
|
|
99
|
-
```bash
|
|
100
|
-
honey generate # writes src/_gen/
|
|
101
|
-
honey generate --watch
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## What Honey is
|
|
105
|
-
|
|
106
|
-
Honey is a single TypeScript builder that becomes a Web-standard `fetch(request, env)` handler. The same app runs on Bun, Node, Deno, and Cloudflare Workers.
|
|
107
|
-
|
|
108
|
-
- **Builder DX.** `honey().get("/users/:id").input(...).handler(...)` is how you write routes. Types flow from path params, Standard Schema input, output maps, and middleware additions into `ctx`.
|
|
109
|
-
- **Radix tree.** Routes compile into a radix tree. Production can load a generated tree so unknown paths 404 without walking a catch-all.
|
|
110
|
-
- **Web Standards.** Handlers see `Request` and return `Response`. `app.fetch()` is the public entry. On Node, `serve()` wraps `IncomingMessage` / `ServerResponse` so the hot path does not build a native Fetch pair.
|
|
111
|
-
- **OpenAPI from the app.** `app.openapi()` serves JSON and YAML from the same document. Scalar or Swagger mounts at `/docs`. Spec and docs routes are internal — they do not appear in the spec they serve.
|
|
112
|
-
- **Clients from the spec.** `honey generate` writes route types, a manifest, OpenAPI, and optional SDKs (TypeScript, Python, Go, Rust) plus an optional Go CLI.
|
|
113
|
-
|
|
114
|
-
Honey is not Express with types bolted on. There is no `req.body` parser stack. Input is declared per route with Standard Schema (Zod, Valibot, ArkType, Effect Schema, Yup — anything that implements the Standard Schema `~standard` interface).
|
|
3
|
+
A typed web framework for the Fetch API. One app runs on Node, Bun and Cloudflare Workers, and its routes generate
|
|
4
|
+
an OpenAPI 3.1 document, a route manifest and, through
|
|
5
|
+
[`@lovrozagar/honey-clients`](https://www.npmjs.com/package/@lovrozagar/honey-clients), typed SDKs in six languages, a
|
|
6
|
+
command-line client and an MCP server.
|
|
115
7
|
|
|
116
8
|
## Install
|
|
117
9
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
bun add @lovrozagar/honey
|
|
122
|
-
# or
|
|
123
|
-
npm add @lovrozagar/honey
|
|
124
|
-
# or
|
|
125
|
-
pnpm add @lovrozagar/honey
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The CLI ships with the package (`honey` in `package.json` `bin`). After install:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
honey init
|
|
132
|
-
honey generate
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
The published package depends only on `jiti` (for generate/watch). Validation libraries are your choice. Two optional peer dependencies:
|
|
136
|
-
|
|
137
|
-
- `ws`, for WebSocket and realtime routes on **Node** (`npm install ws`). Bun, Deno and Workers have WebSockets built in. Without it, a WebSocket upgrade on Node answers 500 and logs the install hint.
|
|
138
|
-
- `ts-morph`, as a **dev** dependency, for type generation (`codegen.types`).
|
|
139
|
-
|
|
140
|
-
## First app
|
|
141
|
-
|
|
142
|
-
`honey init` is the supported scaffold.
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
mkdir my-api && cd my-api
|
|
146
|
-
bun init -y
|
|
147
|
-
bun add @lovrozagar/honey
|
|
148
|
-
honey init
|
|
149
|
-
bun run dev
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
That writes:
|
|
153
|
-
|
|
154
|
-
| File | Role |
|
|
155
|
-
| ---------------- | -------------------------------------------------------- |
|
|
156
|
-
| `src/app.ts` | App export: health route + `openapi({ docs: "scalar" })` |
|
|
157
|
-
| `src/server.ts` | `await app.serve({ port })` |
|
|
158
|
-
| `vite.config.ts` | `honey({ app: "src/app.ts" })` plugin |
|
|
159
|
-
|
|
160
|
-
Scripts added to `package.json`: `dev` (runs `src/server.ts`) and `generate` (`honey generate`).
|
|
161
|
-
|
|
162
|
-
Cloudflare from the start:
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
honey init --cf
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Also writes `src/worker.ts` (`export default { fetch }`) and `wrangler.jsonc`. Existing files refuse to overwrite unless you pass `--force`.
|
|
169
|
-
|
|
170
|
-
Minimal app without the scaffold:
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
import { honey } from "@lovrozagar/honey"
|
|
174
|
-
import * as z from "zod"
|
|
175
|
-
|
|
176
|
-
const app = honey<{ DATABASE_URL: string }>()
|
|
177
|
-
.basePath("/api")
|
|
178
|
-
.trailingSlash("strip")
|
|
179
|
-
.get("/health")
|
|
180
|
-
.handler((ctx) => ctx.res.json("ok", { status: "ok" }))
|
|
181
|
-
.post("/users")
|
|
182
|
-
.input({ json: z.object({ email: z.string().email(), name: z.string() }) })
|
|
183
|
-
.output({ "application/json": { created: z.object({ id: z.string() }) } })
|
|
184
|
-
.handler((ctx) => ctx.res.json("created", { id: "u-1" }))
|
|
185
|
-
|
|
186
|
-
await app.serve({ env: { DATABASE_URL: process.env.DATABASE_URL! }, port: 3000 })
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
## CLI
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
honey generate [--watch] [--config <path>] [--plugin <n>] [--app <path>] [flags]
|
|
193
|
-
honey init [--cf] [--force]
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Flags take `--flag value` or `--flag=value`. Unknown flags, stray arguments, a missing value and a `--config` that does not exist are errors: the CLI prints usage and exits `1`. Any other first argument does the same.
|
|
197
|
-
|
|
198
|
-
### `honey init`
|
|
199
|
-
|
|
200
|
-
| Flag | Meaning |
|
|
201
|
-
| --------- | ----------------------------------------------------------------------- |
|
|
202
|
-
| `--cf` | Also write a Workers entry and `wrangler.jsonc` (alias `--cloudflare`) |
|
|
203
|
-
| `--force` | Overwrite `src/app.ts`, `src/server.ts`, `vite.config.ts` if they exist |
|
|
204
|
-
|
|
205
|
-
`honey init` adds `@lovrozagar/honey` to `dependencies`. In an existing `package.json` it keeps your `dev` / `generate` scripts and `type` unless you pass `--force`.
|
|
206
|
-
|
|
207
|
-
### `honey generate`
|
|
208
|
-
|
|
209
|
-
Reads the Vite `honey()` plugin config (default `vite.config.ts`) and writes artifacts next to the app. Jiti loads the app and the Vite config with `fsCache` and `moduleCache` off, so a second generate after you add a route is not stale.
|
|
10
|
+
1.0 is out as a release candidate, published under npm's `next` tag, so an install without a tag still gives
|
|
11
|
+
0.7.3. Add `@next` until the final release:
|
|
210
12
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
The CLI loads the app on the runtime that started it. Started from a Bun script (`bun run generate`, `bunx honey`), it runs under that Bun even though the bin's shebang is `node`, so an app that relies on Bun's module loading (text imports such as `.md`, Bun-only APIs) generates as it runs. Under Node it stays on Node. Set `HONEY_NO_BUN_HANDOFF=1` to keep Node.
|
|
214
|
-
|
|
215
|
-
A route tree that is stale or was written by an older honey does not stop the generation that replaces it: while `honey generate` loads the app, `routeTree()` ignores such a tree and the routes the app registers are used instead. Outside generation the stale tree still fails with an error naming the routes.
|
|
216
|
-
|
|
217
|
-
| Flag | Meaning |
|
|
218
|
-
| ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
219
|
-
| `--watch` | Regenerate when the route tree checksum changes. Ignores `_gen` / `.gen.*`. Requires `--app` or plugin `app`. |
|
|
220
|
-
| `--config <path>` | Vite config to load (default `vite.config.ts`) |
|
|
221
|
-
| `--plugin <n>` | Which `honey()` plugin in the config, 0-based, when it holds several |
|
|
222
|
-
| `--app <path>` | App module, overrides plugin `app` |
|
|
223
|
-
| `--tree` | Enable writing the generated route tree |
|
|
224
|
-
| `--types` | Enable writing TypeScript route types (needs `ts-morph`) |
|
|
225
|
-
| `--manifest` | Enable writing `manifest.gen.json` |
|
|
226
|
-
| `--sdk` | Enable TypeScript SDK at `src/_gen` (same as `codegen.sdk: true`) |
|
|
227
|
-
| `--cli` | Enable Go CLI; **requires** `--cli-out` and `--cli-binary-name` |
|
|
228
|
-
| `--merge-tree <path>` | Merge this generated tree into the write |
|
|
229
|
-
| `--cli-out <dir>` | Go CLI output directory |
|
|
230
|
-
| `--cli-binary-name <name>` | Binary name |
|
|
231
|
-
| `--cli-config-name <name>` | Optional config file name |
|
|
232
|
-
| `--cli-default-base-url <url>` | Default base URL baked into the CLI |
|
|
233
|
-
| `--cli-env-prefix <prefix>` | Env prefix for CLI config |
|
|
234
|
-
| `--cli-module-path <path>` | Go module path |
|
|
235
|
-
| `--cli-sdk-module-path <path>` | Import an existing Go SDK instead of embedding one |
|
|
236
|
-
|
|
237
|
-
CLI boolean flags **turn features on**. They do not turn plugin-configured features off. Plugin config is the source of truth for paths and SDK ports.
|
|
238
|
-
|
|
239
|
-
Default plugin resolution (when a flag/`true` enables the feature):
|
|
240
|
-
|
|
241
|
-
| Artifact | Default path | Default on? |
|
|
242
|
-
| ---------------------- | -------------------------------------------- | ---------------------------------- |
|
|
243
|
-
| Route tree | `src/_gen/routes.gen.ts` | yes (`tree` defaults on) |
|
|
244
|
-
| Types | `src/_gen/types.gen.d.ts` | no |
|
|
245
|
-
| Manifest | `src/_gen/manifest.gen.json` | no |
|
|
246
|
-
| OpenAPI | `src/_gen/openapi.gen.json` (+ YAML sibling) | no, until `codegen.openApi` is set |
|
|
247
|
-
| TS SDK | `src/_gen/sdk.*.gen.ts` | no |
|
|
248
|
-
| Python / Go / Rust SDK | only if `codegen.sdk.ports` is set | no |
|
|
249
|
-
| Go CLI | only if `codegen.cli` object is set | no |
|
|
250
|
-
|
|
251
|
-
Typical plugin config:
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
import { honey } from "@lovrozagar/honey/plugin"
|
|
255
|
-
|
|
256
|
-
export default {
|
|
257
|
-
plugins: [
|
|
258
|
-
honey({
|
|
259
|
-
app: "src/app.ts",
|
|
260
|
-
watch: ["src/**/*.ts"],
|
|
261
|
-
codegen: {
|
|
262
|
-
tree: true,
|
|
263
|
-
types: true,
|
|
264
|
-
manifest: true,
|
|
265
|
-
openApi: { title: "My API", version: "1.0.0" },
|
|
266
|
-
sdk: {
|
|
267
|
-
name: "MySDK",
|
|
268
|
-
ports: {
|
|
269
|
-
typescript: { outDir: "src/_gen" },
|
|
270
|
-
python: { outDir: "sdk/python" },
|
|
271
|
-
go: { outDir: "sdk/go", modulePath: "example.com/myapi" },
|
|
272
|
-
rust: { outDir: "sdk/rust", crateName: "myapi" },
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
cli: { out: "cli", binaryName: "myapi" },
|
|
276
|
-
},
|
|
277
|
-
}),
|
|
278
|
-
],
|
|
279
|
-
}
|
|
13
|
+
```sh
|
|
14
|
+
npm install @lovrozagar/honey@next
|
|
280
15
|
```
|
|
281
16
|
|
|
282
|
-
`
|
|
283
|
-
|
|
284
|
-
`codegen.invalidate` (default `"warn"`) lists mutations that declare no `invalidate` but have
|
|
285
|
-
sibling read routes they plausibly affect. `"error"` to fail the build; `"off"` to silence it.
|
|
286
|
-
|
|
287
|
-
`generateFromApp(app)` (from `@lovrozagar/honey/plugin`) is the in-process helper: it returns `{ routeTree, manifest?, openApi?, openApiYaml? }` without writing files.
|
|
17
|
+
`bun add` and `pnpm add` work too. Node 22.12 or later, Bun 1.3 or later, or Cloudflare Workers.
|
|
288
18
|
|
|
289
|
-
##
|
|
19
|
+
## Start
|
|
290
20
|
|
|
291
|
-
|
|
21
|
+
`npx honey init` writes `src/app.ts`, `src/server.ts` and a Vite config (`--cf` adds a Worker and its
|
|
22
|
+
`wrangler.jsonc`). The app it writes:
|
|
292
23
|
|
|
293
24
|
```ts
|
|
294
25
|
import { honey } from "@lovrozagar/honey"
|
|
26
|
+
import { serveOpenApi } from "@lovrozagar/honey/openapi"
|
|
295
27
|
|
|
296
|
-
const app =
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
### Routes and methods
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
app.get("/items")
|
|
305
|
-
app.post("/items")
|
|
306
|
-
app.put("/items/:id")
|
|
307
|
-
app.patch("/items/:id")
|
|
308
|
-
app.delete("/items/:id")
|
|
309
|
-
app.head("/items/:id")
|
|
310
|
-
app.options("/items")
|
|
311
|
-
app.all("/echo") // every method
|
|
312
|
-
app.on(["GET"], "/health") // methods are always a list
|
|
313
|
-
app.on(["GET", "HEAD"], "/resource") // same handler, extra methods
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
Each verb returns a **route builder**. Chain `.input()`, `.output()`, `.errors()`, `.boundary()`, `.meta()`, then finish with `.handler()` or `.proxy()`. After `.handler()` you are back on the app, so you can keep chaining routes.
|
|
317
|
-
|
|
318
|
-
There is no `.head` convenience beyond `.head(path)` itself. Use `.on(["GET", "HEAD"], path)` when GET and HEAD share a handler.
|
|
319
|
-
|
|
320
|
-
### Path matching
|
|
321
|
-
|
|
322
|
-
```ts
|
|
323
|
-
app.get("/orgs/:orgId/members/:memberId").handler((ctx) => {
|
|
324
|
-
ctx.params.orgId
|
|
325
|
-
ctx.params.memberId
|
|
326
|
-
return ctx.res.json("ok", ctx.params)
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
app.get("/files/*path").handler((ctx) => {
|
|
330
|
-
// GET /files/a/b/c → ctx.params.path === "a/b/c"
|
|
331
|
-
return ctx.res.json("ok", { path: ctx.params.path })
|
|
332
|
-
})
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
- `:name` is one whole segment. Names use letters, digits, `_`, `$` and `-`; `/f/:name.json` is rejected at registration.
|
|
336
|
-
- `:name?` is an optional last segment: `/items/:id?` serves `/items` and `/items/42`.
|
|
337
|
-
- `*name` (or bare `*`, read as `ctx.params["*"]`) is the remainder of the path and must be the last segment.
|
|
338
|
-
- Patterns are normalized: `admin/x`, `//admin/x` and `/admin/x/` all register `/admin/x`; `basePath("/api/")` is `/api`. `.` and `..` segments, query strings and fragments are rejected.
|
|
339
|
-
- Precedence at each segment: static, then param, then wildcard. When the preferred branch dead-ends — no route further down, or no route for the request's method — the router backtracks to the next one. With `GET /users/me/settings` and `GET /users/:id/profile`, `/users/me/profile` reaches the second; with `GET /u/me` and `DELETE /u/:id`, `DELETE /u/me` reaches the second. A 405 lists every method of every route that matches the path.
|
|
340
|
-
- Static paths (`/health`) use an O(1) map. Dynamic and wildcard segments walk the radix tree.
|
|
341
|
-
|
|
342
|
-
#### Request paths
|
|
343
|
-
|
|
344
|
-
Every request path is normalized once, before anything looks at it. The router, scoped middleware, `ctx.path`, `staticFiles()` and `proxy()` all see the same result, on every runtime:
|
|
345
|
-
|
|
346
|
-
- Empty segments collapse: `//admin///users` is `/admin/users`.
|
|
347
|
-
- Dot segments resolve: `/files/../admin` is `/admin`, never above the root; `%2e` counts as a dot.
|
|
348
|
-
- A backslash is a `/`, and characters a URL path cannot hold raw (space, controls, `"`, `<`, `>`, `` ` ``, `{`, `}`, non-ASCII) are percent-encoded — what URL parsing does, so Deno (which hands the app the raw target) and Bun or Node (which parse it) route the same bytes the same way.
|
|
349
|
-
- An encoded `/` or `\` (`%2F`, `%5C`) is **400**: a segment that decodes to a separator means one thing to this router and another to a proxy upstream or a file system. `app.encodedSlashes("allow")` keeps them encoded in the path so they decode into a param (`/repos/group%2Fproject` gives `id === "group/project"`); only allow them when nothing downstream treats a decoded slash as a separator.
|
|
350
|
-
- Everything else stays percent-encoded in `ctx.path`; params are decoded when extracted.
|
|
351
|
-
- A trailing slash is kept; `trailingSlash()` decides what it means.
|
|
352
|
-
|
|
353
|
-
On Node, `serve()` also answers **400** for a `Host` header that is not `host[:port]` and for request targets other than `/path` or `http(s)://host/path`; on Deno, `serve()` (and `Deno.serve(app.fetch)`) reject such a `Host`, since Deno builds the request URL from it.
|
|
354
|
-
|
|
355
|
-
Optional extra validation of params (beyond “it is a string”):
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
app
|
|
359
|
-
.get("/orgs/:orgId")
|
|
360
|
-
.input({ params: z.object({ orgId: z.string().uuid() }) })
|
|
361
|
-
.handler((ctx) => ctx.res.json("ok", { id: ctx.input.params.orgId }))
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Prefixes and slashes
|
|
365
|
-
|
|
366
|
-
```ts
|
|
367
|
-
const api = app.basePath("/api") // routes registered on `api` are prefixed
|
|
368
|
-
app.trailingSlash("strip") // 308 /health/ → /health (Location is relative: /health)
|
|
369
|
-
app.trailingSlash("enforce") // 308 /health → /health/
|
|
370
|
-
app.trailingSlash("ignore") // both match (default)
|
|
371
|
-
app.stripPrefix("/app") // inbound /app/api/x is matched as /api/x
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
- `basePath` returns a new handle; only routes registered through it (or handles derived from it) are prefixed.
|
|
375
|
-
- `stripPrefix` is a gateway rewrite. Requests without the prefix still match. It will not strip a partial segment (`/apple` is not stripped by `/app`).
|
|
376
|
-
- `.use(mw)` / `.basePath()` / `.context()` / `.meta()` return a new **handle** on the same app. A route captures its handle's chain, prefix, context values and meta when it is registered; serving any handle of the app behaves the same. The handle you call them on is unchanged, so `app.use(auth)` as a bare statement installs nothing — finalize (the first request, `toRouteTree()` or codegen) throws when a `use(mw)` handle never registers, mounts or serves anything, and when a route builder never got `.handler()`.
|
|
377
|
-
- **Lifecycle.** An app is built, then finalized: the first request, `toRouteTree()` or codegen resolves every route's full middleware chain, scopes and error keys once into a per-app table, and requests only look routes up in it. Registering a route, a scope or a mount afterwards (dev/HMR patching, lazy plugins) is allowed: it moves the app to a new epoch, and the next request finalizes again. Served documents cache per epoch too. Finalize is where misconfiguration surfaces — a `use(mw)` handle that registers nothing, a builder without `.handler()`, a stale generated tree — as an error naming the route.
|
|
378
|
-
- Settings — `trailingSlash`, `stripPrefix`, `encodedSlashes`, `trustProxy`, `errorFactory`, `defaultErrors`, `defaultBoundary`, `outputValidation`, `onError`, `onNotFound`, `onMethodNotAllowed`, error formatters, `errorI18n`, `logger`, `telemetry`, `tap`, `wsAdapter` — belong to the app: calling one on any handle applies to every handle and every route, registered before or after.
|
|
379
|
-
|
|
380
|
-
### Context
|
|
381
|
-
|
|
382
|
-
`ctx` is a `HoneyContext`. Fields:
|
|
383
|
-
|
|
384
|
-
| Field | Meaning |
|
|
385
|
-
| ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
386
|
-
| `ctx.req` | Web `Request`; on Node serve, a Request-shaped view (see below) |
|
|
387
|
-
| `ctx.res` | `HoneyRes` — see [Responses](#responses) |
|
|
388
|
-
| `ctx.env` | Bindings you passed to `fetch` / `serve` |
|
|
389
|
-
| `ctx.params` | Path params (`:id`, `*path`) |
|
|
390
|
-
| `ctx.search` / `ctx.searchAll` | First value / all values of the query string (lazy) |
|
|
391
|
-
| `ctx.headers` / `ctx.cookies` | Lazy records (lowercase cookie names as sent) |
|
|
392
|
-
| `ctx.ip` | Client IP, canonical; see [Client address](#client-address) |
|
|
393
|
-
| `ctx.input` | Validated input when `.input()` is declared |
|
|
394
|
-
| `ctx.errors` | Typed error factory when `.errorFactory()` is set |
|
|
395
|
-
| `ctx.meta` | Merged route + chain `.meta()` |
|
|
396
|
-
| `ctx.path` | Normalized request path after `stripPrefix` |
|
|
397
|
-
| `ctx.signal` | Aborts on disconnect, `timeout()` or shutdown; see [SSE and streaming](#sse-and-streaming) |
|
|
398
|
-
| `ctx.routePattern` | Registered pattern, e.g. `/users/:id` |
|
|
399
|
-
| `ctx.realtime` | `{ publish, namespace(name).publish }` when realtime routes exist |
|
|
400
|
-
| `ctx.tap(key, payload)` | Queue a tap (only if `.taps()` was declared) |
|
|
401
|
-
| `ctx.background(promise)` | `waitUntil` on Workers, otherwise fire-and-forget |
|
|
402
|
-
| `ctx.executionCtx` | Workers `ExecutionContext` when `fetch` received one |
|
|
403
|
-
| `ctx.log` | Present when `logger({ instance })` middleware ran |
|
|
404
|
-
| `ctx.requestId` | Present when `requestId()` middleware ran |
|
|
405
|
-
| `ctx.timing` | Present when `serverTiming()` middleware ran |
|
|
406
|
-
|
|
407
|
-
Reserved keys that middleware / `.context()` **cannot** overwrite: every field above that Honey sets (`req`, `res`, `env`, `params`, `headers`, `cookies`, `search`, `searchAll`, `ip`, `path`, `signal`, `meta`, `errors`, …).
|
|
408
|
-
|
|
409
|
-
`ctx.search`, `ctx.searchAll`, `ctx.headers` and validated `search`/`headers`/`form` records have no prototype, so a query like `?__proto__=x` or `?constructor=x` is plain data. Read them with `ctx.search.key`, `key in ctx.search` or `Object.hasOwn(ctx.search, key)`; they have no `hasOwnProperty` method.
|
|
410
|
-
|
|
411
|
-
On Node, `ctx.req` reads the `IncomingMessage` directly instead of building a Fetch `Request` per request. It behaves like one (headers, body methods, `clone()`, `signal`; the same conformance tests run against both), but it is not one: `ctx.req instanceof Request` is `false`, and `new Request(ctx.req)` or `fetch(ctx.req)` throw. Pass it through `toFetchRequest(ctx.req)` (from `@lovrozagar/honey`) first; on other runtimes that returns `ctx.req` itself. The Fetch Request then owns the body, so read it through one or the other.
|
|
412
|
-
|
|
413
|
-
#### Client address
|
|
414
|
-
|
|
415
|
-
`ctx.ip` is the client's address in canonical form (`::ffff:1.2.3.4` is `1.2.3.4`, IPv6 lowercase and compressed), or `null` when unknown. `clientInfo(ctx)` (from `@lovrozagar/honey`) also returns the `protocol` and `host` the client used. One app setting decides all three, and every feature that needs them (`ipRestrict`, loggers, `proxy()`) reads the same answer:
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
import "@lovrozagar/honey/trust" // needed for a hop count or a list; not for the default
|
|
419
|
-
|
|
420
|
-
app.trustProxy(false) // default: the TCP peer is the client; X-Forwarded-* is ignored
|
|
421
|
-
app.trustProxy(1) // one reverse proxy in front, appending to X-Forwarded-For (nginx, most load balancers)
|
|
422
|
-
app.trustProxy(["10.0.0.0/8", "fd00::/8"]) // proxies recognized by address; the first other hop is the client
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
- The peer address comes from the runtime: the socket on Node, `server.requestIP()` on Bun, `info.remoteAddr` on Deno, `CF-Connecting-IP` on Cloudflare Workers (the edge sets it; a client cannot). `app.fetch(req, env)` without a server reports no peer; in tests pass `testClient(app, { env, ip: "203.0.113.1" })`.
|
|
426
|
-
- With a hop count, the client is the `X-Forwarded-For` entry the outermost trusted proxy wrote; entries further left were sent by the client and are never read. With ranges, trusted hops are skipped from the right. `X-Forwarded-Proto` and `X-Forwarded-Host` are read only through a trusted hop.
|
|
427
|
-
- A value at the position the setting reads that is not an address makes `ctx.ip` `null`, never a guess. `X-Real-IP`, `Forwarded` and `True-Client-IP` are not read.
|
|
428
|
-
|
|
429
|
-
```ts
|
|
430
|
-
app
|
|
431
|
-
.context({ version: "1.0.0" })
|
|
432
|
-
.get("/")
|
|
433
|
-
.handler((ctx) => {
|
|
434
|
-
return ctx.res.json("ok", { version: ctx.version })
|
|
435
|
-
})
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
`ctx.res.json("ok", data)` is branded at the type level (`TypedResponse<"application/json", "ok">`) and is a real `Response` on Bun / Workers. On Node `serve()`, known-size bodies skip `new Response()` and write with `writeHead` / `end`.
|
|
439
|
-
|
|
440
|
-
### Responses
|
|
441
|
-
|
|
442
|
-
Every `ctx.res.*` method takes a **status key** (except `noContent`, `redirect`, `sse`, `stream`, `generate`, `raw`). Optional third argument (or last for the exceptions):
|
|
443
|
-
|
|
444
|
-
```ts
|
|
445
|
-
type ResponseOptions = {
|
|
446
|
-
status?: number // override numeric status (redirect only by default)
|
|
447
|
-
headers?: Record<string, string>
|
|
448
|
-
cookies?: Record<string, CookieOptions>
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
type CookieOptions = {
|
|
452
|
-
value: string
|
|
453
|
-
domain?: string
|
|
454
|
-
expires?: Date
|
|
455
|
-
httpOnly?: boolean
|
|
456
|
-
maxAge?: number
|
|
457
|
-
path?: string
|
|
458
|
-
sameSite?: "lax" | "none" | "strict"
|
|
459
|
-
secure?: boolean
|
|
460
|
-
}
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
```ts
|
|
464
|
-
ctx.res.json("ok", { id: "1" })
|
|
465
|
-
ctx.res.json(
|
|
466
|
-
"created",
|
|
467
|
-
{ id: "1" },
|
|
468
|
-
{
|
|
469
|
-
headers: { "x-request-id": "r1" },
|
|
470
|
-
cookies: { sid: { value: "abc", httpOnly: true, sameSite: "lax", path: "/" } },
|
|
471
|
-
},
|
|
28
|
+
export const app = serveOpenApi(
|
|
29
|
+
honey()
|
|
30
|
+
.get("/health")
|
|
31
|
+
.handler((ctx) => ctx.res.text("ok", "ok")),
|
|
32
|
+
{ docs: "scalar", title: "Honey", version: "0.0.1" },
|
|
472
33
|
)
|
|
473
|
-
ctx.res.text("ok", "hello")
|
|
474
|
-
ctx.res.html("ok", "<h1>Hi</h1>")
|
|
475
|
-
ctx.res.csv("ok", "id,name\n1,Ada")
|
|
476
|
-
ctx.res.xml("ok", "<ok/>")
|
|
477
|
-
ctx.res.binary("ok", new Uint8Array([0x48, 0x49]))
|
|
478
|
-
ctx.res.noContent()
|
|
479
|
-
ctx.res.redirect("/elsewhere") // 302
|
|
480
|
-
ctx.res.redirect("/gone", { status: 301 })
|
|
481
|
-
ctx.res.raw(new Response("passthrough"))
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
`__Host-` cookies require `secure: true`, `path: "/"`, and no `domain`.
|
|
485
|
-
|
|
486
|
-
SSE / stream / generate: see [SSE and streaming](#sse-and-streaming).
|
|
487
|
-
|
|
488
|
-
### Status keys
|
|
489
|
-
|
|
490
|
-
`ctx.res.json("created", data)` sets HTTP 201. Use the snake_case key, not the number, except `redirect({ status })`.
|
|
491
|
-
|
|
492
|
-
| Key | Code | Key | Code |
|
|
493
|
-
| --------------------------------- | ---- | --------------------------------- | ---- |
|
|
494
|
-
| `ok` | 200 | `bad_request` | 400 |
|
|
495
|
-
| `created` | 201 | `unauthorized` | 401 |
|
|
496
|
-
| `accepted` | 202 | `payment_required` | 402 |
|
|
497
|
-
| `non_authoritative_information` | 203 | `forbidden` | 403 |
|
|
498
|
-
| `no_content` | 204 | `not_found` | 404 |
|
|
499
|
-
| `reset_content` | 205 | `method_not_allowed` | 405 |
|
|
500
|
-
| `partial_content` | 206 | `not_acceptable` | 406 |
|
|
501
|
-
| `multi_status` | 207 | `proxy_authentication_required` | 407 |
|
|
502
|
-
| `already_reported` | 208 | `request_timeout` | 408 |
|
|
503
|
-
| `im_used` | 226 | `conflict` | 409 |
|
|
504
|
-
| `multiple_choices` | 300 | `gone` | 410 |
|
|
505
|
-
| `moved_permanently` | 301 | `length_required` | 411 |
|
|
506
|
-
| `found` | 302 | `precondition_failed` | 412 |
|
|
507
|
-
| `see_other` | 303 | `content_too_large` | 413 |
|
|
508
|
-
| `not_modified` | 304 | `uri_too_long` | 414 |
|
|
509
|
-
| `temporary_redirect` | 307 | `unsupported_media_type` | 415 |
|
|
510
|
-
| `permanent_redirect` | 308 | `range_not_satisfiable` | 416 |
|
|
511
|
-
| `internal_server_error` | 500 | `expectation_failed` | 417 |
|
|
512
|
-
| `not_implemented` | 501 | `im_a_teapot` | 418 |
|
|
513
|
-
| `bad_gateway` | 502 | `misdirected_request` | 421 |
|
|
514
|
-
| `service_unavailable` | 503 | `unprocessable_entity` | 422 |
|
|
515
|
-
| `gateway_timeout` | 504 | `locked` | 423 |
|
|
516
|
-
| `http_version_not_supported` | 505 | `failed_dependency` | 424 |
|
|
517
|
-
| `variant_also_negotiates` | 506 | `too_early` | 425 |
|
|
518
|
-
| `insufficient_storage` | 507 | `upgrade_required` | 426 |
|
|
519
|
-
| `loop_detected` | 508 | `precondition_required` | 428 |
|
|
520
|
-
| `not_extended` | 510 | `too_many_requests` | 429 |
|
|
521
|
-
| `network_authentication_required` | 511 | `request_header_fields_too_large` | 431 |
|
|
522
|
-
| | | `unavailable_for_legal_reasons` | 451 |
|
|
523
|
-
|
|
524
|
-
`.output()` only accepts **2xx success keys** plus a `redirect` map. Error statuses belong on `.errors()`, not `.output()`.
|
|
525
|
-
|
|
526
|
-
### Input
|
|
527
|
-
|
|
528
|
-
`.input()` takes Standard Schema objects. `json` and `form` are mutually exclusive (one Content-Type per request).
|
|
529
|
-
|
|
530
|
-
```ts
|
|
531
|
-
app
|
|
532
|
-
.put("/in/all/:resourceId")
|
|
533
|
-
.input({
|
|
534
|
-
json: z.object({ title: z.string() }),
|
|
535
|
-
search: z.object({ draft: z.coerce.boolean().optional() }),
|
|
536
|
-
headers: z.object({ "x-request-id": z.string() }),
|
|
537
|
-
cookies: z.object({ sid: z.string() }),
|
|
538
|
-
params: z.object({ resourceId: z.string().min(1) }),
|
|
539
|
-
})
|
|
540
|
-
.handler((ctx) => {
|
|
541
|
-
ctx.input.json.title
|
|
542
|
-
ctx.input.search.draft
|
|
543
|
-
ctx.input.headers["x-request-id"]
|
|
544
|
-
ctx.input.cookies.sid
|
|
545
|
-
ctx.input.params.resourceId
|
|
546
|
-
return ctx.res.json("ok", { ok: true })
|
|
547
|
-
})
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
Form + file (Zod `z.file()` or equivalent):
|
|
551
|
-
|
|
552
|
-
```ts
|
|
553
|
-
app
|
|
554
|
-
.post("/upload")
|
|
555
|
-
.input({ form: z.object({ title: z.string(), upload: z.file() }) })
|
|
556
|
-
.handler((ctx) => ctx.res.json("ok", { name: ctx.input.form.upload.name, title: ctx.input.form.title }))
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
Leave the body for the handler (no Honey parse):
|
|
560
|
-
|
|
561
|
-
```ts
|
|
562
|
-
import { readableStream } from "@lovrozagar/honey/input"
|
|
563
|
-
|
|
564
|
-
app
|
|
565
|
-
.post("/pipe")
|
|
566
|
-
.input({ json: readableStream(z.unknown()) })
|
|
567
|
-
.handler(async (ctx) => {
|
|
568
|
-
const body = ctx.req.body
|
|
569
|
-
return ctx.res.json("ok", { piped: body !== null })
|
|
570
|
-
})
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
Content-Type selection (case-insensitive, parameters ignored):
|
|
574
|
-
|
|
575
|
-
- `application/json` or any `+json` type (`application/vnd.api+json`) → `json`
|
|
576
|
-
- `application/x-www-form-urlencoded` or `multipart/form-data` → `form` (`req.formData()`)
|
|
577
|
-
- Anything else — a missing Content-Type, a look-alike such as `application/jsonx`, or a header with two media types — is **415** `unsupported_media_type`.
|
|
578
|
-
- A body the declared parser cannot read (malformed JSON or multipart, an empty JSON body) is **400** with `error_key: "malformed_body"` and `vars.format`. `onError` sees it as that 400 `HoneyError`.
|
|
579
|
-
- Invalid input is **400** with `error_key: "validation_failed"` and `fields: { name: [{ error_key, message, path }] }`.
|
|
580
|
-
|
|
581
|
-
A `json` or `form` schema is validated for every method that declares it, `DELETE` and `OPTIONS` included (a missing body is 415). Declaring one on a `GET` or `HEAD` route throws at registration — put that data in `search`.
|
|
582
|
-
|
|
583
|
-
Repeated keys in `search` and `form` follow one policy, read from the schema's JSON Schema (Zod 4.2+, ArkType):
|
|
584
|
-
|
|
585
|
-
- A key the schema types as an array always gets an array, even for one value — so a single file validates against `z.array(z.instanceof(File))`.
|
|
586
|
-
- A key typed as a scalar gets the **first** value, the same one `ctx.search.key` returns.
|
|
587
|
-
- With schemas that expose no JSON Schema (Valibot, Yup, Effect), one value is a scalar and a repeated key is an array.
|
|
588
|
-
|
|
589
|
-
```ts
|
|
590
|
-
// ?tag=a&tag=b&page=1&page=2
|
|
591
|
-
app
|
|
592
|
-
.get("/items")
|
|
593
|
-
.input({ search: z.object({ tag: z.array(z.string()), page: z.coerce.number() }) })
|
|
594
|
-
.handler((ctx) => ctx.res.json("ok", ctx.input.search)) // { tag: ["a", "b"], page: 1 }
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
Cookies with a repeated name keep the first; headers follow Fetch (repeated values joined with `, `).
|
|
598
|
-
|
|
599
|
-
Valibot / ArkType work the same way — pass any Standard Schema:
|
|
600
|
-
|
|
601
|
-
```ts
|
|
602
|
-
import * as v from "valibot"
|
|
603
|
-
app
|
|
604
|
-
.post("/v")
|
|
605
|
-
.input({ json: v.object({ n: v.number() }) })
|
|
606
|
-
.handler((ctx) => ctx.res.json("ok", ctx.input.json))
|
|
607
|
-
```
|
|
608
|
-
|
|
609
|
-
### Output
|
|
610
|
-
|
|
611
|
-
`.output()` maps content type → status key → schema. Only 2xx keys plus `redirect`.
|
|
612
|
-
|
|
613
|
-
```ts
|
|
614
|
-
app
|
|
615
|
-
.post("/items")
|
|
616
|
-
.output({
|
|
617
|
-
"application/json": {
|
|
618
|
-
created: z.object({ id: z.string() }),
|
|
619
|
-
ok: z.object({ id: z.string() }),
|
|
620
|
-
},
|
|
621
|
-
})
|
|
622
|
-
.handler((ctx) => ctx.res.json("created", { id: "1" }))
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
Declared content types unlock the matching `ctx.res` method at the type level:
|
|
626
|
-
|
|
627
|
-
| Content type | Method |
|
|
628
|
-
| -------------------------- | ------------------------------------- |
|
|
629
|
-
| `application/json` | `json` |
|
|
630
|
-
| `text/plain` | `text` |
|
|
631
|
-
| `text/html` | `html` |
|
|
632
|
-
| `text/csv` | `csv` |
|
|
633
|
-
| `application/xml` | `xml` |
|
|
634
|
-
| `application/octet-stream` | `binary` |
|
|
635
|
-
| `text/event-stream` | `sse` |
|
|
636
|
-
| `redirect` | `redirect` (map of 3xx keys → `true`) |
|
|
637
|
-
|
|
638
|
-
Always available: `noContent`, `raw`, `redirect`, `stream`. Also recognized in the map (no dedicated helper): `application/cbor`, `application/msgpack`, `application/pdf`.
|
|
639
|
-
|
|
640
|
-
```ts
|
|
641
|
-
app
|
|
642
|
-
.get("/go")
|
|
643
|
-
.output({ redirect: { found: true, moved_permanently: true } })
|
|
644
|
-
.handler((ctx) => ctx.res.redirect("/next"))
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
`.outputValidation("off" | "dev" | "always")` controls runtime checks. `"dev"` (typical) validates when `NODE_ENV !== "production"`. Mismatch is **500** (`output_validation_failed` or `output_content_type_mismatch`).
|
|
648
|
-
|
|
649
|
-
```ts
|
|
650
|
-
app.outputValidation("always")
|
|
651
34
|
```
|
|
652
35
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
<!-- snippet:name errors-app -->
|
|
36
|
+
And the server, `serve(app)` from `@lovrozagar/honey/serve` on Node and Bun:
|
|
656
37
|
|
|
657
38
|
```ts
|
|
658
|
-
import {
|
|
659
|
-
|
|
660
|
-
const errors = defineErrors({
|
|
661
|
-
unauthorized: "unauthorized",
|
|
662
|
-
org_slug_taken: "conflict",
|
|
663
|
-
item_not_found: {
|
|
664
|
-
status: "not_found",
|
|
665
|
-
schema: z.object({ reason: z.string() }),
|
|
666
|
-
},
|
|
667
|
-
api_error: "internal_server_error",
|
|
668
|
-
orgs_unavailable: "service_unavailable",
|
|
669
|
-
})
|
|
39
|
+
import { honey } from "@lovrozagar/honey"
|
|
40
|
+
import { serve } from "@lovrozagar/honey/serve"
|
|
670
41
|
|
|
671
42
|
const app = honey()
|
|
672
|
-
|
|
673
|
-
.defaultErrors("unauthorized") // every route may throw these
|
|
674
|
-
.defaultBoundary("api_error") // unexpected throws become this key
|
|
675
|
-
.onError((error, ctx) => {
|
|
676
|
-
if (error instanceof HoneyError && error.errorKey === "org_slug_taken") {
|
|
677
|
-
return ctx.jsonFromError(error)
|
|
678
|
-
}
|
|
679
|
-
return undefined // fall through to default handling
|
|
680
|
-
})
|
|
681
|
-
.onNotFound((ctx) => ctx.jsonFromError(errors.item_not_found({ reason: "no route" })))
|
|
682
|
-
.onMethodNotAllowed((ctx) => {
|
|
683
|
-
// ctx.allowed is the Allow list
|
|
684
|
-
return new Response(null, { status: 405, headers: { allow: ctx.allowed.join(", ") } })
|
|
685
|
-
})
|
|
686
|
-
```
|
|
687
|
-
|
|
688
|
-
Throw from a handler:
|
|
689
|
-
|
|
690
|
-
```ts
|
|
691
|
-
throw ctx.errors.org_slug_taken({ vars: { slug: "acme" } })
|
|
692
|
-
throw ctx.errors.item_not_found({ reason: "deleted" })
|
|
693
|
-
throw ctx.errors.unauthorized({
|
|
694
|
-
fields: { token: [{ error_key: "required", message: "missing", path: "token" }] },
|
|
695
|
-
headers: { "www-authenticate": "Bearer" },
|
|
696
|
-
})
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
Standard error JSON (matches OpenAPI / generated `_ErrEnvelope`):
|
|
700
|
-
|
|
701
|
-
```json
|
|
702
|
-
{
|
|
703
|
-
"error_key": "org_slug_taken",
|
|
704
|
-
"status": 409,
|
|
705
|
-
"status_key": "conflict",
|
|
706
|
-
"message": "org_slug_taken",
|
|
707
|
-
"success": false,
|
|
708
|
-
"fields": {}
|
|
709
|
-
}
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
`vars` are **ICU input only** — used by `.errorI18n()` to interpolate the top-level `message`, and included in `HoneyError.serialize` for logs. They are **not** part of the public envelope. With i18n on, clients read the interpolated `message` (e.g. `"Slug acme is taken"`); they must not re-translate from `vars` + `error_key`.
|
|
713
|
-
|
|
714
|
-
For structured client extras (numbers a CLI can chart, typed payloads), use a **custom-schema** error (`defineErrors({ key: { status, schema } })`) and its `data` body — not `vars`.
|
|
715
|
-
|
|
716
|
-
Custom-schema errors serialize **the schema payload** as the body (not the envelope), unless you set `customErrorFormatter`.
|
|
717
|
-
|
|
718
|
-
Per-route:
|
|
719
|
-
|
|
720
|
-
<!-- snippet:continue errors-app -->
|
|
721
|
-
|
|
722
|
-
```ts
|
|
723
|
-
app
|
|
724
|
-
.post("/orgs")
|
|
725
|
-
.errors("org_slug_taken") // default errors ("unauthorized") need no listing
|
|
726
|
-
.boundary("orgs_unavailable") // this route's unexpected-throw key
|
|
727
|
-
.handler((ctx) => {
|
|
728
|
-
throw ctx.errors.org_slug_taken({ vars: { slug: "x" } })
|
|
729
|
-
})
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
`.errors(factory, ...keys)` also accepts the factory object as the first argument (kitchen style). Undeclared `HoneyError` keys fail the boundary check and become the boundary error.
|
|
733
|
-
|
|
734
|
-
Formatters:
|
|
735
|
-
|
|
736
|
-
```ts
|
|
737
|
-
app.defaultErrorFormatter((error, defaultShape) => ({
|
|
738
|
-
...defaultShape,
|
|
739
|
-
request_id: "r1",
|
|
740
|
-
}))
|
|
741
|
-
|
|
742
|
-
app.defaultErrorFormatter(z.object({ error_key: z.string(), status: z.number() }), (error) => ({
|
|
743
|
-
error_key: error.errorKey,
|
|
744
|
-
status: error.status,
|
|
745
|
-
}))
|
|
746
|
-
|
|
747
|
-
app.customErrorFormatter((error, data) => ({ ...data, error_key: error.errorKey }))
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
i18n (loads `@lovrozagar/honey/i18n` when called):
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
import "@lovrozagar/honey/i18n"
|
|
754
|
-
|
|
755
|
-
app.errorI18n({
|
|
756
|
-
errors: {
|
|
757
|
-
en: { org_slug_taken: "Slug {slug} is taken", unauthorized: "Sign in" },
|
|
758
|
-
de: { org_slug_taken: "Name {slug} ist vergeben", unauthorized: "Anmeldung nötig" },
|
|
759
|
-
},
|
|
760
|
-
fieldNames: {
|
|
761
|
-
en: { email: "Email" },
|
|
762
|
-
},
|
|
763
|
-
resolveLocale: (ctx) => {
|
|
764
|
-
const accept = ctx.req.headers.get("accept-language")
|
|
765
|
-
return accept?.startsWith("de") ? "de" : "en"
|
|
766
|
-
},
|
|
767
|
-
})
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
Messages are ICU: `{slug}`, `{n, number}`, `{n, plural, one {# item} other {# items}}`, `{k, select, a {A} other {X}}`. Pass matching values as `vars` on the throw. `cause` is reserved and is not a template var. Field `path` labels use `fieldNames`; field `message` strings are not interpolated with top-level `vars`.
|
|
771
|
-
|
|
772
|
-
`HoneyError` fields: `errorKey`, `status` (number), `statusKey`, `fields`, `vars` (ICU / logs only), `data`, `headers`. `HoneyError.serialize(err)` is a JSON-safe dump for logs (includes `vars` when set; never put on the public JSON).
|
|
773
|
-
|
|
774
|
-
Framework-owned keys you do not declare: `validation_failed` (400), `malformed_body` (400), `bad_request` (400, e.g. an encoded `/` in the path), `forbidden` (403, `csrf` / `ipRestrict`), `output_validation_failed` (500), plus routing / content-negotiation keys (`not_found`, `method_not_allowed`, `unsupported_media_type`, `content_too_large`, …).
|
|
775
|
-
|
|
776
|
-
### Middleware
|
|
777
|
-
|
|
778
|
-
```ts
|
|
779
|
-
import { createMiddleware, defineErrors, honey } from "@lovrozagar/honey"
|
|
780
|
-
|
|
781
|
-
const errors = defineErrors({ unauthorized: "unauthorized" })
|
|
782
|
-
const app = honey().errorFactory(errors)
|
|
783
|
-
|
|
784
|
-
const withAuth = createMiddleware(async (ctx: { req: Request }, next) => {
|
|
785
|
-
const token = ctx.req.headers.get("authorization")
|
|
786
|
-
if (!token) throw errors.unauthorized()
|
|
787
|
-
return next({ user: { id: "u-1" } })
|
|
788
|
-
})
|
|
789
|
-
|
|
790
|
-
const authed = app.use(withAuth) // routes registered on `authed` run withAuth
|
|
791
|
-
authed.get("/me").handler((ctx) => ctx.res.json("ok", ctx.user))
|
|
792
|
-
|
|
793
|
-
app.use("/admin", withAuth) // scoped: every request under /admin, whichever handle registered the route
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
`createMiddleware` infers additions from `next({ ... })`. Later handlers see `ctx.user`. Return `next()` with no argument to add nothing. Type the `ctx` parameter with the fields the middleware reads (`{ req: Request }`); a middleware can run on any route, so it throws errors from the factory itself (`errors.unauthorized()`) rather than `ctx.errors`, which handlers get typed. Without an error factory, throw a `HoneyError`.
|
|
797
|
-
|
|
798
|
-
`.use(mw)` returns a new handle and leaves the one it is called on unchanged: keep the returned value (or keep chaining) and register routes on it. `app.use(mw)` as a bare statement installs nothing, and the first request (or codegen) throws to say so.
|
|
799
|
-
|
|
800
|
-
What a route runs is fixed when it is registered or mounted, in this order: the chain of the handle it was registered on (a mounting handle's chain first), then every scope that covers it, then its own `.get(...).use(mw)`, then input validation.
|
|
801
|
-
|
|
802
|
-
- **Scopes guard request paths.** `app.use("/admin", mw)` uses the route grammar (`/orgs/:id`, `/admin/*`) and covers every request whose path is under it — including requests that reach `all("/*")` or `/:section/users`, where the request path is checked at runtime. It applies to routes registered before or after it, on any handle, and to mounted sub-apps.
|
|
803
|
-
- **Errors become responses at every `next()`.** A throw in a handler or a middleware is turned into the error response at that layer, so every middleware around it gets a `Response` from `await next()` and its post-`next()` code (CORS headers, request id, logging, timing) runs on errors too. A `try/catch` around `next()` no longer sees throws: read `ctx.error` instead — the `HoneyError` behind the error response (`errorKey`, `status`, and `cause`, the original throw when it was not a `HoneyError`), `undefined` when nothing failed. 404 and 405 are plain responses and leave it unset.
|
|
804
|
-
|
|
805
|
-
```ts
|
|
806
|
-
const metrics = createMiddleware(async (ctx, next) => {
|
|
807
|
-
const res = await next()
|
|
808
|
-
record(res.status, ctx.error?.errorKey)
|
|
809
|
-
return res
|
|
810
|
-
})
|
|
811
|
-
```
|
|
812
|
-
|
|
813
|
-
- **404, 405 and CORS preflight** run the same pipeline: the middleware every route of the app starts with, plus the scopes covering the request path, with `ctx.errors`, `ctx.path` and context values set. A preflight (`OPTIONS` with `Access-Control-Request-Method`) runs the chain of the route for the requested method, never its handler.
|
|
814
|
-
- Middleware `errors` and `meta` count for every route the middleware runs on — chain, scoped or route level.
|
|
815
|
-
- **Mounting** (`parent.route(sub)`): the sub's routes keep the sub's error factory, default errors, boundary, output validation, taps and context values; the parent's `onError`, formatters, logger and telemetry serve them. A sub-app's own `fetch` keeps serving the sub alone — mounting copies its routes and never changes it.
|
|
816
|
-
|
|
817
|
-
### Shipped middleware
|
|
818
|
-
|
|
819
|
-
Import each from its path. Do **not** `import { cors } from "@lovrozagar/honey"`. Each example keeps the handle `use()` returns — register routes on `api`. To run one for the whole app, put it at the start of the chain every route is registered on (`const app = honey().use(cors())`).
|
|
820
|
-
|
|
821
|
-
#### `cors` — `@lovrozagar/honey/cors`
|
|
822
|
-
|
|
823
|
-
```ts
|
|
824
|
-
import { cors } from "@lovrozagar/honey/cors"
|
|
825
|
-
|
|
826
|
-
const api = app.use(cors())
|
|
827
|
-
const custom = app.use(
|
|
828
|
-
cors({
|
|
829
|
-
origin: "https://app.example.com", // or "*" | string[] | (origin) => boolean
|
|
830
|
-
credentials: true, // needs an explicit origin (string, list or predicate); with "*" or none, cors() throws
|
|
831
|
-
methods: ["GET", "POST"],
|
|
832
|
-
headers: ["authorization", "content-type"],
|
|
833
|
-
exposeHeaders: ["x-request-id"],
|
|
834
|
-
maxAge: 86400,
|
|
835
|
-
}),
|
|
836
|
-
)
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
No `Origin` header → no CORS headers are added. Preflight is `OPTIONS` + `access-control-request-method`. `app.serve({ cors: true })` runs `cors()` with defaults before every route, 404 and preflight of the app. `app.serve({ cors: { origin } })` passes the object through, and throws at `serve()` for `credentials: true` without an origin.
|
|
840
|
-
|
|
841
|
-
- `credentials: true` requires an explicit `origin`: a wildcard would let every site make credentialed requests and read the answers, so construction throws.
|
|
842
|
-
- The opaque origin `null` (sandboxed iframes, `file:`) is never reflected; any page can produce it.
|
|
843
|
-
- Every response carries `Vary: Origin` unless the policy is a plain `"*"`, so a shared cache never serves one origin's answer to another. 101 upgrade responses pass through untouched.
|
|
844
|
-
- What it does not cover: CORS only decides what a browser lets a page _read_. It does not stop a cross-site form post or `no-cors` fetch from reaching the handler — use `csrf()` for that.
|
|
845
|
-
|
|
846
|
-
#### `csrf` — `@lovrozagar/honey/csrf`
|
|
847
|
-
|
|
848
|
-
```ts
|
|
849
|
-
import { csrf } from "@lovrozagar/honey/csrf"
|
|
850
|
-
|
|
851
|
-
const api = app.use(csrf({ origin: "https://app.example.com" }))
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
Every unsafe request is checked, whatever its Content-Type (the algorithm of Go's `http.CrossOriginProtection`):
|
|
855
|
-
|
|
856
|
-
1. `GET`, `HEAD` and `OPTIONS` pass.
|
|
857
|
-
2. With `Sec-Fetch-Site` (every current browser sends it): `same-origin` and `none` pass. `same-site` and `cross-site` pass only for an `Origin` in `origin` (a string, list or predicate) — a sibling subdomain is same-site but not trusted.
|
|
858
|
-
3. Without it: a request with no `Origin` passes (curl, server-to-server, clients a browser cannot drive cross-site), an `Origin` whose host equals `Host` passes, an allow-listed `Origin` passes, anything else is rejected.
|
|
859
|
-
|
|
860
|
-
Failure is **403** `forbidden`. Cross-origin browser callers of your JSON API must be listed in `origin`. What it does not cover: safe methods (keep `GET` side-effect free), and same-origin attackers (XSS).
|
|
861
|
-
|
|
862
|
-
#### `body-limit` — `@lovrozagar/honey/body-limit`
|
|
863
|
-
|
|
864
|
-
```ts
|
|
865
|
-
import { bodyLimit } from "@lovrozagar/honey/body-limit"
|
|
43
|
+
app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
|
|
866
44
|
|
|
867
|
-
|
|
868
|
-
bodyLimit({
|
|
869
|
-
maxSize: 1_048_576,
|
|
870
|
-
limits: { "application/json": 64_000, "multipart/": 10_485_760 },
|
|
871
|
-
trustContentLength: false, // default; true skips counting when Content-Length is in range
|
|
872
|
-
}),
|
|
873
|
-
)
|
|
45
|
+
await serve(app, { port: Number(process.env.PORT ?? 3000) })
|
|
874
46
|
```
|
|
875
47
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
```ts
|
|
881
|
-
import { createLogger, logger } from "@lovrozagar/honey/logger"
|
|
882
|
-
|
|
883
|
-
const log = createLogger({
|
|
884
|
-
level: "info", // trace | debug | info | warn | error | fatal
|
|
885
|
-
base: { service: "api" },
|
|
886
|
-
write: (line) => console.log(line),
|
|
887
|
-
})
|
|
888
|
-
|
|
889
|
-
const api = app.use(logger())
|
|
890
|
-
const custom = app.use(
|
|
891
|
-
logger({
|
|
892
|
-
instance: log, // pino-shaped; sets ctx.log
|
|
893
|
-
skip: (data) => data.path === "/health",
|
|
894
|
-
log: (data) => {
|
|
895
|
-
// used when instance is omitted
|
|
896
|
-
console.log(`${data.method} ${data.path} ${data.status} ${data.duration}ms`)
|
|
897
|
-
},
|
|
898
|
-
}),
|
|
899
|
-
)
|
|
48
|
+
```sh
|
|
49
|
+
curl http://127.0.0.1:3000/health
|
|
50
|
+
# ok
|
|
51
|
+
# the API reference: open http://127.0.0.1:3000/docs
|
|
900
52
|
```
|
|
901
53
|
|
|
902
|
-
`
|
|
903
|
-
|
|
904
|
-
#### `curl-logger` — `@lovrozagar/honey/curl-logger`
|
|
905
|
-
|
|
906
|
-
```ts
|
|
907
|
-
import { curlLogger, defaultRedactHeader, defaultRedactQueryParam } from "@lovrozagar/honey/curl-logger"
|
|
908
|
-
|
|
909
|
-
const api = app.use(
|
|
910
|
-
curlLogger({
|
|
911
|
-
body: { maxBytes: 2048, allowContentTypes: ["application/json"] },
|
|
912
|
-
redactHeader: (name, value) => (name === "x-tenant" ? "***" : defaultRedactHeader(name, value)),
|
|
913
|
-
redactQueryParam: (name, value) => defaultRedactQueryParam(name, value),
|
|
914
|
-
skip: (data) => data.path === "/health", // { duration, method, path, requestId, status }
|
|
915
|
-
}),
|
|
916
|
-
)
|
|
917
|
-
```
|
|
918
|
-
|
|
919
|
-
Credentials are masked by default: `authorization`, `cookie`, `set-cookie`, `proxy-authorization`, API-key and token headers, and token-like query params (`token`, `access_token`, `api_key`, `sig`, `code`, …) print as `[REDACTED]`. A custom `redactHeader` / `redactQueryParam` replaces the default — compose with the exported `defaultRedactHeader` / `defaultRedactQueryParam` to extend it; return `null` to drop the header or param. `skip` runs before the curl command is built. Every part of the command is shell-quoted (`-X 'POST'`, `$'…'` for control characters), so a logged command is safe to paste. A failing callback never fails the request.
|
|
920
|
-
|
|
921
|
-
#### `request-id` — `@lovrozagar/honey/request-id`
|
|
922
|
-
|
|
923
|
-
```ts
|
|
924
|
-
import { requestId } from "@lovrozagar/honey/request-id"
|
|
925
|
-
|
|
926
|
-
const api = app.use(requestId())
|
|
927
|
-
const custom = app.use(requestId({ header: "x-request-id", generator: () => crypto.randomUUID() }))
|
|
928
|
-
```
|
|
929
|
-
|
|
930
|
-
Adds `ctx.requestId` and echoes the header on the response. Reuses an inbound id only when it is 1–128 characters of `A-Z a-z 0-9 . _ : + / = -` (UUIDs, ULIDs, base64, W3C trace ids); anything else is replaced with a generated id, so a client cannot put arbitrary text into your logs. `validate: (id) => boolean` overrides the check.
|
|
931
|
-
|
|
932
|
-
#### `etag` — `@lovrozagar/honey/etag`
|
|
933
|
-
|
|
934
|
-
```ts
|
|
935
|
-
import { etag } from "@lovrozagar/honey/etag"
|
|
936
|
-
|
|
937
|
-
const api = app.use(etag()) // weak ETag (default)
|
|
938
|
-
const custom = app.use(etag({ weak: false }))
|
|
939
|
-
```
|
|
940
|
-
|
|
941
|
-
GET/HEAD only, and only on 200 responses. Never reads a stream: `sse()`, `stream()`, `generate()` and native bodies without a `content-length` pass through untagged. A handler-set `ETag` is kept. Responds **304** when `If-None-Match` matches (weak comparison, lists and `*`).
|
|
942
|
-
|
|
943
|
-
#### `timeout` — `@lovrozagar/honey/timeout`
|
|
944
|
-
|
|
945
|
-
```ts
|
|
946
|
-
import { timeout } from "@lovrozagar/honey/timeout"
|
|
947
|
-
|
|
948
|
-
const api = app.use(timeout({ duration: 5_000 }))
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
Slow handlers reject with **504** `request_timeout`, and `ctx.signal` aborts with a `TimeoutError` so the handler's work stops: pass the signal to `fetch()`, your database driver or anything else that takes one. Work that ignores it runs to completion and its response is dropped.
|
|
952
|
-
|
|
953
|
-
#### `secure-headers` — `@lovrozagar/honey/secure-headers`
|
|
954
|
-
|
|
955
|
-
```ts
|
|
956
|
-
import { secureHeaders } from "@lovrozagar/honey/secure-headers"
|
|
957
|
-
|
|
958
|
-
const api = app.use(secureHeaders())
|
|
959
|
-
const custom = app.use(
|
|
960
|
-
secureHeaders({
|
|
961
|
-
contentSecurityPolicy: "default-src 'self'",
|
|
962
|
-
strictTransportSecurity: "max-age=63072000; includeSubDomains",
|
|
963
|
-
permissionsPolicy: "camera=()",
|
|
964
|
-
referrerPolicy: "strict-origin-when-cross-origin", // or false to omit
|
|
965
|
-
xContentTypeOptions: "nosniff", // or false
|
|
966
|
-
xFrameOptions: "SAMEORIGIN",
|
|
967
|
-
xXssProtection: "0",
|
|
968
|
-
crossOriginOpenerPolicy: "same-origin",
|
|
969
|
-
crossOriginEmbedderPolicy: "require-corp",
|
|
970
|
-
crossOriginResourcePolicy: "same-site",
|
|
971
|
-
}),
|
|
972
|
-
)
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
Defaults when omitted: `x-content-type-options: nosniff`, `x-frame-options: SAMEORIGIN`, `referrer-policy: strict-origin-when-cross-origin`, `x-xss-protection: 0`. A header the handler already set wins (a route's stricter CSP is never overwritten). Works on immutable responses (`fetch()`, `Response.redirect()`) and leaves 101 upgrades alone.
|
|
976
|
-
|
|
977
|
-
#### `server-timing` — `@lovrozagar/honey/server-timing`
|
|
978
|
-
|
|
979
|
-
```ts
|
|
980
|
-
import { serverTiming } from "@lovrozagar/honey/server-timing"
|
|
981
|
-
|
|
982
|
-
const api = app.use(serverTiming())
|
|
983
|
-
api.get("/work").handler((ctx) => {
|
|
984
|
-
ctx.timing.start("db", "query")
|
|
985
|
-
ctx.timing.end("db")
|
|
986
|
-
return ctx.res.json("ok", {})
|
|
987
|
-
})
|
|
988
|
-
```
|
|
989
|
-
|
|
990
|
-
#### `ip-restrict` — `@lovrozagar/honey/ip-restrict`
|
|
991
|
-
|
|
992
|
-
```ts
|
|
993
|
-
import { ipRestrict } from "@lovrozagar/honey/ip-restrict"
|
|
994
|
-
import "@lovrozagar/honey/trust"
|
|
995
|
-
|
|
996
|
-
const api = app
|
|
997
|
-
.trustProxy(1) // behind one reverse proxy; see Client address
|
|
998
|
-
.use(
|
|
999
|
-
ipRestrict({
|
|
1000
|
-
allowList: ["127.0.0.1", "10.0.0.0/8"],
|
|
1001
|
-
denyList: ["192.168.1.50"],
|
|
1002
|
-
}),
|
|
1003
|
-
)
|
|
1004
|
-
```
|
|
1005
|
-
|
|
1006
|
-
- The client address is `ctx.ip`, decided by `app.trustProxy()` — see [Client address](#client-address). `getIp: (req) => ...` replaces it with your own source.
|
|
1007
|
-
- Rules and addresses are compared as numbers, so `::ffff:203.0.113.7`, `203.0.113.7` and `203.0.113.7:51234` are one address, and `2001:DB8::1` equals `2001:db8::1`.
|
|
1008
|
-
- Construction throws on an invalid rule (`10.0.0.0/33`, `010.0.0.1`, `abc`), on `allowList: []` (it would reject everyone), and with no list at all.
|
|
1009
|
-
- Fails closed: a request whose address is unknown or not an address is **403**, for allow and deny lists alike. Denied / not-allowed is **403**.
|
|
1010
|
-
- What it does not cover: it trusts whatever `trustProxy()` says. Behind a proxy you did not declare, every request has the proxy's address.
|
|
1011
|
-
|
|
1012
|
-
#### `powered-by` — `@lovrozagar/honey/powered-by`
|
|
1013
|
-
|
|
1014
|
-
```ts
|
|
1015
|
-
import { poweredBy } from "@lovrozagar/honey/powered-by"
|
|
1016
|
-
|
|
1017
|
-
const api = app.use(poweredBy()) // x-powered-by: Honey
|
|
1018
|
-
const custom = app.use(poweredBy({ name: "api" }))
|
|
1019
|
-
```
|
|
1020
|
-
|
|
1021
|
-
#### `pretty-json` — `@lovrozagar/honey/pretty-json`
|
|
1022
|
-
|
|
1023
|
-
```ts
|
|
1024
|
-
import { prettyJson } from "@lovrozagar/honey/pretty-json"
|
|
1025
|
-
|
|
1026
|
-
const api = app.use(prettyJson()) // ?pretty=
|
|
1027
|
-
const custom = app.use(prettyJson({ query: "pretty", space: 2 }))
|
|
1028
|
-
```
|
|
1029
|
-
|
|
1030
|
-
Rewrites buffered `application/json` (and `+json`) bodies when the query string has a parameter with exactly that name (`?pretty`, `?pretty=1`; not `?prettyx`). Streams, empty bodies and invalid JSON pass through untouched.
|
|
1031
|
-
|
|
1032
|
-
#### What the middleware does not cover
|
|
1033
|
-
|
|
1034
|
-
| Middleware | Does not cover |
|
|
1035
|
-
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1036
|
-
| `cors` | Requests reaching the handler: it only controls what a browser lets a page read. Pair it with `csrf` for cookie-authenticated writes. |
|
|
1037
|
-
| `csrf` | Safe methods (keep `GET` side-effect free), same-origin attackers (XSS), non-browser clients (they have no ambient credentials to abuse). |
|
|
1038
|
-
| `ipRestrict` | Proxies you did not declare in `trustProxy()`: behind one, every request has the proxy's address. |
|
|
1039
|
-
| `bodyLimit` | Bodies a handler reads before the middleware runs. Node and Bun `serve()` add their own cap (`maxRequestBodySize`). |
|
|
1040
|
-
| `timeout` | Work that ignores `ctx.signal` keeps running after the 504. |
|
|
1041
|
-
| `secureHeaders` | Content it does not see: a header the handler set wins, and the CSP is yours to write. |
|
|
1042
|
-
| `staticFiles` | What `resolve` does with the path: it is safe to join onto a root, but symlinks inside the root are followed if your file API follows them. |
|
|
1043
|
-
| `proxy()` | Where requests go: build the URL from a fixed origin in `destination`, never from request data. Upstream responses are passed through, not filtered. |
|
|
1044
|
-
| `requestId` | Uniqueness of inbound ids: a client can still send an id that collides with another request's. |
|
|
1045
|
-
| loggers | Secrets in bodies (`curlLogger({ body })`) and in header or param names the default patterns miss; extend the redact callbacks. |
|
|
1046
|
-
|
|
1047
|
-
### Composition
|
|
1048
|
-
|
|
1049
|
-
```ts
|
|
1050
|
-
const users = honey()
|
|
1051
|
-
.basePath("/users")
|
|
1052
|
-
.get("/:id")
|
|
1053
|
-
.handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
|
|
1054
|
-
|
|
1055
|
-
const app = honey().route(users) // merges routes, realtime, taps, static map
|
|
1056
|
-
const v1 = honey().route("/v1", users) // serves /v1/users/:id
|
|
1057
|
-
```
|
|
1058
|
-
|
|
1059
|
-
`.route(sub)` copies the sub-app's routes into this one, under the chain of the handle you call it on: `app.use(auth).route(admin)` runs `auth` on every admin route. The sub's paths land under that handle's `basePath`, after the sub's own: `app.basePath("/v1").route(users)` serves `/v1/users/:id`, and `route("/v1", users)` is the same thing. The sub's scoped middleware and realtime routes move with it. Duplicate paths throw.
|
|
1060
|
-
|
|
1061
|
-
### Taps
|
|
1062
|
-
|
|
1063
|
-
Taps run **after** a successful handler. They do not run on thrown errors.
|
|
1064
|
-
|
|
1065
|
-
```ts
|
|
1066
|
-
const app = honey()
|
|
1067
|
-
.taps<{ audit: { action: string } }>()
|
|
1068
|
-
.tap("audit", (ctx, payload) => {
|
|
1069
|
-
ctx.background(
|
|
1070
|
-
fetch("https://logs.example.com", {
|
|
1071
|
-
method: "POST",
|
|
1072
|
-
body: JSON.stringify({ path: ctx.req.url, ...payload }),
|
|
1073
|
-
}),
|
|
1074
|
-
)
|
|
1075
|
-
})
|
|
1076
|
-
.post("/items")
|
|
1077
|
-
.handler((ctx) => {
|
|
1078
|
-
ctx.tap("audit", { action: "create" })
|
|
1079
|
-
return ctx.res.json("created", { id: "1" })
|
|
1080
|
-
})
|
|
1081
|
-
```
|
|
1082
|
-
|
|
1083
|
-
### Proxy routes
|
|
1084
|
-
|
|
1085
|
-
Finish a route with `.proxy()` instead of `.handler()`. The proxy is not part of the core bundle: import its entry once in the app, or `.proxy()` throws at registration.
|
|
1086
|
-
|
|
1087
|
-
```ts
|
|
1088
|
-
import "@lovrozagar/honey/proxy"
|
|
1089
|
-
|
|
1090
|
-
app.all("/upstream/*path").proxy({
|
|
1091
|
-
destination: (ctx, url, init) => fetch(`https://api.internal${url}`, init),
|
|
1092
|
-
rewriteUrl: (url) => url.replace(/^\/upstream/, ""),
|
|
1093
|
-
requestHeaders: { "x-forwarded-by": "honey" },
|
|
1094
|
-
// or requestHeaders: (ctx, headers) => { headers.set("x-user", ctx.user.id) }
|
|
1095
|
-
timeout: 10_000, // time to response headers (default 60 s); 0 for none; disabled for WS upgrades
|
|
1096
|
-
idleTimeout: 30_000, // max gap between body chunks; omit for none (SSE)
|
|
1097
|
-
onResponse: (ctx, response) => {
|
|
1098
|
-
response.headers.set("x-proxied", "1")
|
|
1099
|
-
},
|
|
1100
|
-
})
|
|
1101
|
-
```
|
|
1102
|
-
|
|
1103
|
-
`destination` receives the normalized path + query (after `rewriteUrl`) and a prepared `RequestInit` (method, headers, body for every method but GET/HEAD, a signal following `ctx.signal` and the timeouts, `redirect: "manual"`). Hop-by-hop headers and headers named in `Connection` are stripped both ways; the client's `Host`, `Expect` and forwarding headers are dropped, and `X-Forwarded-For`/`-Proto`/`-Host` describe the client as `trustProxy()` decided (`forwardedHeaders: false` to omit). Encoded responses lose `Content-Encoding`/`Content-Length` because `fetch()` decodes them (`decoded: false` for destinations returning encoded bytes). Network failures answer 502, a headers timeout 504, any other throw 500. `onResponse` gets a mutable copy and is not called for 101 upgrades.
|
|
1104
|
-
|
|
1105
|
-
What it does not cover: `destination` decides where the request goes — build the URL from a fixed origin (as above), never from request data.
|
|
1106
|
-
|
|
1107
|
-
### Static files
|
|
1108
|
-
|
|
1109
|
-
```ts
|
|
1110
|
-
import { staticFiles } from "@lovrozagar/honey/static"
|
|
1111
|
-
|
|
1112
|
-
const site = app.use(
|
|
1113
|
-
staticFiles({
|
|
1114
|
-
prefix: "/assets",
|
|
1115
|
-
resolve: async (_ctx, filePath) => {
|
|
1116
|
-
const file = Bun.file(`./public${filePath}`)
|
|
1117
|
-
if (!(await file.exists())) return null
|
|
1118
|
-
return new Response(file)
|
|
1119
|
-
},
|
|
1120
|
-
headers: { "cache-control": "public, max-age=3600" },
|
|
1121
|
-
// or headers: (filePath) => ({ "content-type": mime(filePath) })
|
|
1122
|
-
rewritePath: (filePath) => (filePath === "/" ? "/index.html" : filePath),
|
|
1123
|
-
}),
|
|
1124
|
-
)
|
|
1125
|
-
```
|
|
1126
|
-
|
|
1127
|
-
GET/HEAD only. `resolve` returning `null` falls through to the next route.
|
|
1128
|
-
|
|
1129
|
-
- `staticFiles` matches `ctx.path` — the normalized path the router saw, after `stripPrefix` — on whole segments: `prefix: "/assets"` serves `/assets/x`, never `/assets-private/x`.
|
|
1130
|
-
- `filePath` is decoded once and contains no `.`, `..`, empty, NUL, `:`, `/` or `\` segments, so joining it onto a root directory cannot leave the root, on POSIX or Windows. Anything else, including malformed percent-encoding, falls through to the next handler. A `rewritePath` result is checked again.
|
|
1131
|
-
- `headers(filePath)` receives the same path `resolve` got. Headers are added copy-on-write, so a `Response` with immutable headers (from `fetch()` or a Workers `ASSETS` binding) works.
|
|
1132
|
-
|
|
1133
|
-
### Logging, telemetry, production tree
|
|
1134
|
-
|
|
1135
|
-
App-level logger (Honey internals, not request logs):
|
|
1136
|
-
|
|
1137
|
-
```ts
|
|
1138
|
-
app.logger({ warn: (msg, ...args) => console.warn(msg, ...args) })
|
|
1139
|
-
```
|
|
1140
|
-
|
|
1141
|
-
OpenTelemetry-shaped adapter:
|
|
1142
|
-
|
|
1143
|
-
```ts
|
|
1144
|
-
import { otelAdapter } from "@lovrozagar/honey/telemetry/otel"
|
|
1145
|
-
|
|
1146
|
-
app.telemetry(otelAdapter({ tracer }))
|
|
1147
|
-
// or a hand-rolled adapter:
|
|
1148
|
-
app.telemetry({
|
|
1149
|
-
onRequest: ({ req }) => {},
|
|
1150
|
-
onRoute: ({ method, path, route, params }) => {},
|
|
1151
|
-
onHandler: ({ route, status, duration }) => {},
|
|
1152
|
-
onResponse: ({ status, duration }) => {},
|
|
1153
|
-
onError: ({ error, duration }) => {},
|
|
1154
|
-
onMiddleware: ({ name, duration, error }) => {},
|
|
1155
|
-
onNotFound: ({ method, path }) => {},
|
|
1156
|
-
onMethodNotAllowed: ({ allowed }) => {},
|
|
1157
|
-
})
|
|
1158
|
-
```
|
|
1159
|
-
|
|
1160
|
-
`onResponse` fires exactly once for every request that fired `onRequest` — trailing-slash redirects, 404, 405, preflights, upgrades and errors included. `route` is the matched pattern (`/users/:id`); otel records it as `http.route` and never puts the query string in an attribute (`url.path`, `http.url` without query or fragment).
|
|
1161
|
-
|
|
1162
|
-
Production tree: load the generated `routes.gen.ts` before registering routes. The tree supplies only the router topology (route ids at the leaves) and per-route data (meta, error keys); every route the app registers afterwards binds to its leaf by `METHOD /pattern`. Several apps can load the same module: it is frozen, and each app serves its own handlers.
|
|
1163
|
-
|
|
1164
|
-
```ts
|
|
1165
|
-
import { routeTree } from "./_gen/routes.gen.ts"
|
|
1166
|
-
|
|
1167
|
-
const app = honey().routeTree(routeTree)
|
|
1168
|
-
app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
|
|
1169
|
-
```
|
|
1170
|
-
|
|
1171
|
-
- A route the app registers that the tree does not hold, or a tree from an older honey, means the generated file is stale: the first request throws an error naming the routes. Run `honey generate`.
|
|
1172
|
-
- A tree leaf the app does not register is answered with 404 but still documented by `app.openapi()`.
|
|
1173
|
-
- Spec, docs and manifest routes are not part of the generated tree; `app.openapi()` mounts them either way.
|
|
1174
|
-
- `routeTree()` must come before any route registration and can be called once.
|
|
1175
|
-
|
|
1176
|
-
`app.toRouteTree()` snapshots an app — a copy of its topology plus each route's data and live handler. `mergeTree` from `honey` / `honey/tree` merges several trees (optional extra meta per tree) without touching its inputs. Loading a snapshot or a merge serves those live handlers; loading the same tree twice gives two independent apps.
|
|
1177
|
-
|
|
1178
|
-
Gateways: a gateway loads a tree that holds downstream routes it does not implement, and registers a root wildcard (`app.all("/*")`) as its catch-all. Every leaf without a local handler is delegated to the catch-all, which then runs with that route's own pattern (`ctx.routePattern`), params, meta and scoped middleware. Paths that are not in the tree 404 — for every method, including `HEAD` — without reaching the catch-all.
|
|
1179
|
-
|
|
1180
|
-
```ts
|
|
1181
|
-
import { routeTree } from "./_gen/routes.gen.ts"
|
|
1182
|
-
|
|
1183
|
-
import "@lovrozagar/honey/proxy"
|
|
1184
|
-
|
|
1185
|
-
const gateway = honey().routeTree(routeTree)
|
|
1186
|
-
gateway.all("/*").proxy({ destination: (ctx) => `https://${ctx.meta.worker}.internal${ctx.path}` })
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
Gateway codegen: point `codegen.mergeTree` at a generate-only module that merges live downstream apps with `app.toRouteTree()`. Generated `routes.gen.ts` omits JSON Schema, so merging another service's generated tree documents no request or response bodies. `honey generate` copies the merged schemas onto the gateway app before it writes OpenAPI; the gateway runtime still serves its own generated tree.
|
|
1190
|
-
|
|
1191
|
-
```ts
|
|
1192
|
-
// gateway/src/route-tree.ts — codegen only, never imported at runtime
|
|
1193
|
-
import { mergeTree } from "@lovrozagar/honey"
|
|
1194
|
-
import { app as usersApp } from "@acme/users/app"
|
|
1195
|
-
|
|
1196
|
-
export const tree = mergeTree([usersApp.toRouteTree(), { worker: "users" }])
|
|
1197
|
-
```
|
|
1198
|
-
|
|
1199
|
-
`app.fetch(request, env, executionCtx?)` is always valid. Sync handlers return a `Response` directly; async handlers and middleware return a `Promise<Response>`. Callers should `await app.fetch(...)`.
|
|
1200
|
-
|
|
1201
|
-
## Serve
|
|
1202
|
-
|
|
1203
|
-
### Bun, Node, Deno
|
|
1204
|
-
|
|
1205
|
-
```ts
|
|
1206
|
-
const handle = await app.serve({
|
|
1207
|
-
port: 3000,
|
|
1208
|
-
hostname: "0.0.0.0",
|
|
1209
|
-
cors: true, // or a CORS options object
|
|
1210
|
-
env: { DATABASE_URL },
|
|
1211
|
-
runtime: "bun", // optional; detected if omitted
|
|
1212
|
-
})
|
|
1213
|
-
|
|
1214
|
-
handle.url // http://127.0.0.1:3000
|
|
1215
|
-
handle.port
|
|
1216
|
-
handle.hostname
|
|
1217
|
-
handle.runtime // "bun" | "node" | "deno"
|
|
1218
|
-
await handle.close() // or close(timeoutMs), default 1000
|
|
1219
|
-
```
|
|
1220
|
-
|
|
1221
|
-
Defaults: `port` 3000, `hostname` `0.0.0.0` (Deno defaults to `127.0.0.1`). Bound `0.0.0.0` / `::` is printed as `127.0.0.1` in `url`.
|
|
1222
|
-
|
|
1223
|
-
More options:
|
|
1224
|
-
|
|
1225
|
-
| Option | Runtimes | Meaning |
|
|
1226
|
-
| -------------------- | --------- | ---------------------------------------------------------------------------------------- |
|
|
1227
|
-
| `maxRequestBodySize` | Node, Bun | Largest request body in bytes; bigger is 413 and the connection closes. Default 128 MiB. |
|
|
1228
|
-
| `headersTimeout` | Node | `server.headersTimeout` (ms) |
|
|
1229
|
-
| `requestTimeout` | Node | `server.requestTimeout` (ms) |
|
|
1230
|
-
| `keepAliveTimeout` | Node | `server.keepAliveTimeout` (ms) |
|
|
1231
|
-
| `upgradeTimeout` | Node | How long an upgrade request may take to become a WebSocket (ms). Default 30 000. |
|
|
1232
|
-
|
|
1233
|
-
`close(timeout?)` is the same on every runtime: stop accepting connections, close WebSockets with 1001, let in-flight requests finish for up to `timeout` ms, then cut what is left (Deno has no forced close: a request still running finishes on its own).
|
|
1234
|
-
|
|
1235
|
-
`app.serve()` keeps an adapter you set with `app.wsAdapter(...)` (for example `nodeWebSocket({ keepalive })`), and attaches the runtime's default adapter otherwise.
|
|
1236
|
-
|
|
1237
|
-
| Runtime | How it listens |
|
|
1238
|
-
| ------- | --------------------------------------------- |
|
|
1239
|
-
| Bun | `Bun.serve({ fetch })` + `honey/ws/bun` |
|
|
1240
|
-
| Node | `node:http` + `honey/serve` + `honey/ws/node` |
|
|
1241
|
-
| Deno | `Deno.serve` + `honey/ws/deno` |
|
|
1242
|
-
|
|
1243
|
-
On Node, import `honey/serve` (or call `app.serve()`, which loads it) so the listen implementation is registered. The Node adapter wraps `IncomingMessage` instead of `new Request()` on the inbound hot path, and writes known JSON/text bodies with `res.end` instead of draining a Fetch `Response`.
|
|
1244
|
-
|
|
1245
|
-
Low-level Node listen (same adapter, no runtime detect):
|
|
1246
|
-
|
|
1247
|
-
```ts
|
|
1248
|
-
import { serve } from "@lovrozagar/honey/node"
|
|
1249
|
-
const server = serve(app, { env: {}, port: 3000, hostname: "0.0.0.0" })
|
|
1250
|
-
await server.shutdown(10_000)
|
|
1251
|
-
```
|
|
1252
|
-
|
|
1253
|
-
`shutdown(timeout?)` stops accepting connections and lets in-flight requests finish. Responses that are already streaming (SSE, `generate()`) never finish on their own, so their `ctx.signal` aborts at once and they end; WebSockets get a 1001 close; handlers still working get until `timeout`, then their signal aborts and every connection, WebSockets included, is closed. Idle keep-alive connections close immediately.
|
|
1254
|
-
|
|
1255
|
-
The low-level `serve()` takes the same `maxRequestBodySize`, `headersTimeout`, `requestTimeout`, `keepAliveTimeout` and `upgradeTimeout` options, plus `onError(err)` for failures the adapter cannot hand to the app (default `console.error`; the client gets 500).
|
|
1256
|
-
|
|
1257
|
-
On Node, bodies Honey built in memory, or that declare a small `content-length`, are written in one `res.end`. Anything else is piped as it is produced, whatever its content type, and the reader is cancelled when the client goes away. A streamed body's `content-length` is never trusted (Node chunks it), and hop-by-hop headers (`Connection`, `Keep-Alive`, `Transfer-Encoding`, and any header `Connection` names) on a Response are dropped: Node writes its own.
|
|
1258
|
-
|
|
1259
|
-
Only a WebSocket handshake takes Node's upgrade path. Any other `Upgrade` request (`h2c`, …) is served as the normal request it is, with its body. A rejected handshake (auth failure, 404, an SSE route) gets a real response with every header and its status text.
|
|
1260
|
-
|
|
1261
|
-
`runtime: "cloudflare"` throws. Workers cannot listen.
|
|
1262
|
-
|
|
1263
|
-
Detect without serving:
|
|
1264
|
-
|
|
1265
|
-
```ts
|
|
1266
|
-
import { detectRuntime } from "@lovrozagar/honey"
|
|
1267
|
-
detectRuntime() // "bun" | "node" | "deno" | "cloudflare"
|
|
1268
|
-
```
|
|
1269
|
-
|
|
1270
|
-
### Cloudflare Workers
|
|
1271
|
-
|
|
1272
|
-
```ts
|
|
1273
|
-
import { cfWebSocket } from "@lovrozagar/honey/ws/cloudflare"
|
|
1274
|
-
import { app } from "./app.ts"
|
|
1275
|
-
|
|
1276
|
-
app.wsAdapter(cfWebSocket())
|
|
1277
|
-
|
|
1278
|
-
export default {
|
|
1279
|
-
fetch: (req: Request, env: Env, ctx: ExecutionContext) => app.fetch(req, env, ctx),
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
`honey init --cf` writes this stub. Local e2e uses wrangler / workerd. The live proof worker is documented under [Live Cloudflare proof](#live-cloudflare-proof).
|
|
1284
|
-
|
|
1285
|
-
Adapters if you wire them yourself: `bunWebSocket` (`honey/ws/bun`), `nodeWebSocket` (`honey/ws/node`), `denoWebSocket` (`honey/ws/deno`), `cfWebSocket` (`honey/ws/cloudflare`). `app.serve()` attaches the matching one.
|
|
1286
|
-
|
|
1287
|
-
### Feature auto-load
|
|
1288
|
-
|
|
1289
|
-
`honey()` stays fetch-only until you opt in:
|
|
1290
|
-
|
|
1291
|
-
| Call or import | Loads |
|
|
1292
|
-
| -------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
1293
|
-
| `app.serve()` or `import "@lovrozagar/honey/serve"` | Listen adapters |
|
|
1294
|
-
| `app.openapi()` / `app.manifest()` or `import "@lovrozagar/honey/openapi"` | Spec + docs (codegen; live Zod) |
|
|
1295
|
-
| `import { spec } from "@lovrozagar/honey/openapi/spec"` | Spec walker (live tree; intern has no JSON Schema) |
|
|
1296
|
-
| `app.errorI18n()` or `import "@lovrozagar/honey/i18n"` | Error i18n |
|
|
1297
|
-
|
|
1298
|
-
Production bundles that only call `app.fetch` do not pull listen, OpenAPI, or i18n code. The load uses an opaque `import(["@lovrozagar/honey", name].join("/"))` so bundlers do not follow unused feature entries.
|
|
1299
|
-
|
|
1300
|
-
`createBuildPlugin` (`honey/build`) is a Vite plugin that injects those imports per target (`bun` | `node` | `deno` | `cloudflare`).
|
|
1301
|
-
|
|
1302
|
-
```ts
|
|
1303
|
-
import { createBuildPlugin } from "@lovrozagar/honey/build"
|
|
1304
|
-
|
|
1305
|
-
createBuildPlugin(
|
|
1306
|
-
{ target: "bun", port: 3000, minify: true, outDir: "dist", external: [] },
|
|
1307
|
-
{ entry: "src/app.ts", export: "app" }, // or export: "default"
|
|
1308
|
-
)
|
|
1309
|
-
```
|
|
1310
|
-
|
|
1311
|
-
Which features a bundle needs is detected from the app's import graph (`.openapi()` / `.manifest()`, `.errorI18n()`, `.serve()` calls). Force one on or off with `features: { openapi: true, serve: false }`.
|
|
1312
|
-
|
|
1313
|
-
## OpenAPI, docs, and manifest
|
|
1314
|
-
|
|
1315
|
-
```ts
|
|
1316
|
-
app.openapi({
|
|
1317
|
-
title: "My API",
|
|
1318
|
-
version: "1.0.0",
|
|
1319
|
-
description: "Optional",
|
|
1320
|
-
docs: "scalar", // or "swagger"
|
|
1321
|
-
docsPath: "/docs", // default /docs, then /reference if a user route owns /docs
|
|
1322
|
-
path: "/openapi", // stem → /openapi.json, .yaml, .yml
|
|
1323
|
-
filterRoutes: (route) => route.path !== "/debug",
|
|
1324
|
-
securitySchemes: {
|
|
1325
|
-
bearerAuth: { type: "http", scheme: "bearer" },
|
|
1326
|
-
},
|
|
1327
|
-
enabled: process.env.NODE_ENV !== "production", // false mounts nothing; default true
|
|
1328
|
-
})
|
|
1329
|
-
|
|
1330
|
-
app.manifest()
|
|
1331
|
-
app.manifest({ path: "/manifest.json" })
|
|
1332
|
-
```
|
|
1333
|
-
|
|
1334
|
-
Each `openapi()` call serves its own document, so an internal and a public profile can live side by side at different paths (`openapi({ path: "/internal/openapi" })` and `openapi({ path: "/openapi", profile: "public" })`). A document is generated once per route change, serialized once, and sent with a strong `ETag` (a matching `If-None-Match` is 304), `Cache-Control: no-cache` and `X-Content-Type-Options: nosniff`. A generation that fails is not retried until the routes change. A path an earlier call or a user route already owns throws at the `openapi()` / `manifest()` call — give a second call its own `path` and `docsPath`.
|
|
1335
|
-
|
|
1336
|
-
The docs UIs load pinned asset versions (Scalar 1.73.1, Swagger UI 5.33.1 from jsDelivr) with Subresource Integrity, under a per-response CSP nonce and `nosniff`; the inline config is escaped. The UI's spec URL includes `stripPrefix`.
|
|
1337
|
-
|
|
1338
|
-
Served (cached, invalidated when the route graph changes):
|
|
1339
|
-
|
|
1340
|
-
| Path | Body |
|
|
1341
|
-
| -------------------------------- | -------------------------------------------------- |
|
|
1342
|
-
| `/openapi.json` | OpenAPI 3.1 |
|
|
1343
|
-
| `/openapi.yaml` / `/openapi.yml` | Same document as YAML |
|
|
1344
|
-
| `/docs` | Scalar or Swagger UI pointing at the JSON spec |
|
|
1345
|
-
| `/manifest.json` | Route methods, paths, middleware names, error keys |
|
|
1346
|
-
|
|
1347
|
-
Those routes are marked internal. They do not appear inside the spec or the manifest. Routes with `.meta({ internal: true })` are left out of the served document and the served manifest too. The served manifest follows the document's visibility policy: it carries only meta keys the `metaSpec` maps, plus the built-ins.
|
|
1348
|
-
|
|
1349
|
-
`.meta({ operationId, tags, summary, description })` on a route feeds the document. App-level `.meta<Shape>()` / `.meta({ auth: "required" })` sets defaults and constrains route meta.
|
|
1350
|
-
|
|
1351
|
-
Generate-time sanitize (plugin `codegen.openApi.sanitize`):
|
|
1352
|
-
|
|
1353
|
-
<!-- snippet:skip -->
|
|
1354
|
-
|
|
1355
|
-
```ts
|
|
1356
|
-
{
|
|
1357
|
-
stripSecuritySchemes: ["legacy"],
|
|
1358
|
-
stripSecurityRequirements: ["legacy"],
|
|
1359
|
-
stripXExtensions: true, // or ["x-internal"]
|
|
1360
|
-
}
|
|
1361
|
-
```
|
|
1362
|
-
|
|
1363
|
-
### Meta spec
|
|
1364
|
-
|
|
1365
|
-
`.metaSpec()` declares what flows from route meta and route schemas into the document. Without it, the built-in policy maps the eight fields above and drops everything else silently.
|
|
1366
|
-
|
|
1367
|
-
```ts
|
|
1368
|
-
const app = honey<Env>()
|
|
1369
|
-
.meta<HoneyMeta<AppRouteMeta>>()
|
|
1370
|
-
.metaSpec({
|
|
1371
|
-
strict: "error", // an app meta key with no entry fails the build
|
|
1372
|
-
meta: {
|
|
1373
|
-
permissions: "x-permissions", // verbatim
|
|
1374
|
-
rateLimit: { key: "x-rate-limit", map: (v) => ({ category: v, rps: RPS[v] }) },
|
|
1375
|
-
worker: false, // deliberately internal — never emitted
|
|
1376
|
-
captcha: false,
|
|
1377
|
-
},
|
|
1378
|
-
schema: {
|
|
1379
|
-
// read off `.meta({ entity })` stamped on the route's schemas, fan out to several tags
|
|
1380
|
-
entity: {
|
|
1381
|
-
from: ["output"],
|
|
1382
|
-
search: "deep", // looks inside `{ articles: [Article], nextCursor }` envelopes
|
|
1383
|
-
expand: (e) => ({
|
|
1384
|
-
"x-entity": e.table,
|
|
1385
|
-
"x-generated": e.generated,
|
|
1386
|
-
"x-soft-delete": e.softDelete ? { field: e.softDelete } : undefined,
|
|
1387
|
-
}),
|
|
1388
|
-
},
|
|
1389
|
-
},
|
|
1390
|
-
profiles: {
|
|
1391
|
-
// allowlist — a tag added later stays out of the public document until opted in
|
|
1392
|
-
public: { include: ["x-entity", "x-query"] },
|
|
1393
|
-
},
|
|
1394
|
-
})
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
Every key of the app's meta type needs an entry — mapped, or `false` — enforced by the type of the `meta` section and again at codegen. Route meta beats schema-derived facts; `meta.extensions` (a record of `x-*` keys) is the escape hatch and outranks both. `profile` on `app.openapi()` or on a `codegen.openApi` entry emits several documents with different extension sets from one policy. Codegen-time only; nothing runs per request.
|
|
1398
|
-
|
|
1399
|
-
Full reference, precedence rules, error taxonomy and migration path: [meta-spec.md](./docs/meta-spec.md).
|
|
1400
|
-
|
|
1401
|
-
#### Middleware-contributed meta
|
|
1402
|
-
|
|
1403
|
-
A fact that middleware already enforces should not be retyped on every route it protects:
|
|
1404
|
-
|
|
1405
|
-
```ts
|
|
1406
|
-
export const shard = createMiddleware(fn, { meta: { tenant: "project_id" } })
|
|
1407
|
-
|
|
1408
|
-
const tenant = app.use(shard) // every route registered on `tenant` — or app.use("/orgs", shard), or .get("/x").use(shard)
|
|
1409
|
-
```
|
|
1410
|
-
|
|
1411
|
-
Explicit `.meta()` (route, then chain) always outranks a contributed value; among middleware, the one that runs later wins (chain, then scoped, then route level — the order they run in). Contributed meta lands in the route's `mt`, so it reaches `ctx.meta`, the manifest, and the policy — with `strict: "error"` a middleware cannot contribute a key that has no entry. A path-scoped middleware registered _after_ the routes it covers still applies to them, and mounted routes get the meta of the chain they are mounted on. `internal` may not be contributed, since a middleware that removed routes from the document would be invisible from both sides.
|
|
1412
|
-
|
|
1413
|
-
Resolved once when the app finalizes (first request, `toRouteTree()` or codegen), never per request, from the same chain the route runs. Change what a middleware contributes, then regenerate.
|
|
1414
|
-
|
|
1415
|
-
## WebSockets
|
|
1416
|
-
|
|
1417
|
-
```ts
|
|
1418
|
-
app.ws("/echo-ws").handler({
|
|
1419
|
-
onOpen(_ctx, ws) {
|
|
1420
|
-
ws.send("connected")
|
|
1421
|
-
ws.send({ hello: true }) // objects are JSON.stringified
|
|
1422
|
-
},
|
|
1423
|
-
onMessage(_ctx, ws, data) {
|
|
1424
|
-
ws.send(data) // string | ArrayBuffer
|
|
1425
|
-
},
|
|
1426
|
-
onReconnect(_ctx, ws, token) {
|
|
1427
|
-
ws.send(JSON.stringify({ event: "reconnected", token }))
|
|
1428
|
-
},
|
|
1429
|
-
onClose(_ctx, _ws, code, reason) {},
|
|
1430
|
-
onError(_ctx, _ws, error) {},
|
|
1431
|
-
})
|
|
1432
|
-
```
|
|
1433
|
-
|
|
1434
|
-
`ws` is `{ send, close, readyState, bufferedAmount, raw }`. `readyState` is `0|1|2|3`. Sends before open are buffered (max 32); sends after close are dropped. `close(code, reason)` cuts a reason longer than 123 UTF-8 bytes to fit. Middleware on the chain runs for the HTTP upgrade, and so does `.input()` validation of search, headers and cookies (400 before the upgrade). Auth middleware works: throw to reject.
|
|
1435
|
-
|
|
1436
|
-
Callbacks run on one ordered queue per connection: `onOpen` (or `onReconnect`), then each message, then `onClose`, each awaited, so a message never runs before an async `onOpen` finished. A callback that throws or rejects goes to `onError`; without `onError`, or when `onError` throws itself, the error is logged (app logger, else `console.error`) and the queue moves on. Nothing a callback throws reaches the runtime. More than 1024 messages waiting for the handler close the connection with 1008.
|
|
1437
|
-
|
|
1438
|
-
**Origin.** Browsers let any page open a socket to any host and send that host's cookies with it. So by default a cross-origin upgrade that carries `Cookie` or `Authorization` gets **403**; same-origin upgrades, and requests without `Origin` (non-browser clients), pass. Allow other origins per route:
|
|
1439
|
-
|
|
1440
|
-
```ts
|
|
1441
|
-
app
|
|
1442
|
-
.ws("/feed")
|
|
1443
|
-
.origins(["https://app.example.com"]) // or "*", or (origin) => boolean
|
|
1444
|
-
.handler({
|
|
1445
|
-
onOpen(_ctx, ws) {
|
|
1446
|
-
ws.send("hi")
|
|
1447
|
-
},
|
|
1448
|
-
})
|
|
1449
|
-
|
|
1450
|
-
app.realtime("/rt", { allowedOrigins: ["https://app.example.com"], handler })
|
|
1451
|
-
```
|
|
1452
|
-
|
|
1453
|
-
"Same origin" compares the `Origin` host with the request host (`clientInfo(ctx).host`, which follows `trustProxy`).
|
|
1454
|
-
|
|
1455
|
-
**Adapter options.** Every adapter takes the same options with the same defaults:
|
|
1456
|
-
|
|
1457
|
-
| Option | Default | Meaning |
|
|
1458
|
-
| -------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1459
|
-
| `maxPayload` | 1 MiB | Largest inbound message; bigger closes with 1009 (Bun drops the connection: 1006). |
|
|
1460
|
-
| `backpressureLimit` | 8 MiB | Unsent outbound bytes before `backpressurePolicy` applies to the next `send()`. |
|
|
1461
|
-
| `backpressurePolicy` | `"close"` | `"close"` closes with 1013 (try again later); `"drop"` discards the message. |
|
|
1462
|
-
| `idleTimeout` | 120 000 ms | Close a connection that received nothing (message or pong) for this long; quiet peers are pinged. `0` disables. Bun and Deno take whole seconds; Workers evicts idle sockets itself. |
|
|
1463
|
-
| `protocol` | first offered | `(offered, req) => string \| null`: the subprotocol to answer with. |
|
|
1464
|
-
|
|
1465
|
-
`nodeWebSocket()` also takes `keepalive: { interval, timeout }`: ping every `interval` ms and terminate a peer whose pong is `timeout` ms late. Binary messages arrive as `ArrayBuffer` on every runtime.
|
|
1466
|
-
|
|
1467
|
-
```ts
|
|
1468
|
-
app.wsAdapter(nodeWebSocket({ maxPayload: 64 * 1024, keepalive: { interval: 30_000, timeout: 10_000 } }))
|
|
1469
|
-
```
|
|
1470
|
-
|
|
1471
|
-
A GET **without** `Upgrade: websocket` to a WS/realtime path returns **426** with `upgrade: websocket`.
|
|
1472
|
-
|
|
1473
|
-
Reconnect: pass `?reconnect_token=` on the next handshake; Honey calls `onReconnect` instead of `onOpen`.
|
|
1474
|
-
|
|
1475
|
-
`app.serve()` attaches the runtime adapter unless you set one. On Node it needs the `ws` package (an optional peer dependency). On Cloudflare, call `app.wsAdapter(cfWebSocket())` yourself. The adapters bring the websocket session runtime with them, so an app without websockets does not bundle it; a custom `WSAdapter` in a bundled build needs `import "@lovrozagar/honey/ws/session"`.
|
|
1476
|
-
|
|
1477
|
-
## Realtime
|
|
1478
|
-
|
|
1479
|
-
A topic bus over WebSockets: connections join topics, and anyone can publish to a topic. Only the WebSocket transport ships, with no resume, replay or acknowledgements. A client that reconnects is a new connection. The realtime server is not part of the core bundle: import `@lovrozagar/honey/realtime` once in the app, or `app.realtime()` throws at registration.
|
|
1480
|
-
|
|
1481
|
-
```ts
|
|
1482
|
-
import "@lovrozagar/honey/realtime"
|
|
1483
|
-
|
|
1484
|
-
const authed = app.use(withAuth)
|
|
1485
|
-
|
|
1486
|
-
authed.realtime("/realtime/chat/:roomId", {
|
|
1487
|
-
namespace: "chat",
|
|
1488
|
-
identify: (ctx) => ctx.user.id, // becomes conn.userId; a throw rejects the upgrade
|
|
1489
|
-
handler: (ctx, conn) => {
|
|
1490
|
-
const room = `room:${ctx.params.roomId}`
|
|
1491
|
-
conn.join(room)
|
|
1492
|
-
conn.send({ event: "joined", id: conn.id, userId: conn.userId })
|
|
1493
|
-
conn.on("message", (payload) => {
|
|
1494
|
-
conn.publish(room, { from: conn.userId, payload })
|
|
1495
|
-
})
|
|
1496
|
-
conn.on("close", () => {
|
|
1497
|
-
conn.publish(room, { left: conn.userId })
|
|
1498
|
-
})
|
|
1499
|
-
},
|
|
1500
|
-
onError: (error, conn) => log.error({ error, conn: conn.id }),
|
|
1501
|
-
})
|
|
1502
|
-
|
|
1503
|
-
// publishing from HTTP needs the same auth as the socket
|
|
1504
|
-
authed.post("/realtime/broadcast/:roomId").handler(async (ctx) => {
|
|
1505
|
-
ctx.realtime.namespace("chat").publish(`room:${ctx.params.roomId}`, await ctx.req.json())
|
|
1506
|
-
return ctx.res.json("ok", { published: true })
|
|
1507
|
-
})
|
|
1508
|
-
```
|
|
1509
|
-
|
|
1510
|
-
**Wire format.** Every frame, in both directions, is one JSON text. `conn.send(x)` and `publish(topic, x)` send `JSON.stringify(x)`, strings included (`"hi"`), and throw for values JSON cannot represent. An inbound text frame is parsed and handed to the `message` handler as is. Non-JSON and binary frames are dropped.
|
|
1511
|
-
|
|
1512
|
-
**Namespaces.** Topics belong to a namespace. It defaults to the route's full path pattern, so two realtime routes never see each other's topics unless both set the same `namespace`. `ctx.realtime.namespace(name).publish(topic, data)` publishes from any request. `ctx.realtime.publish(topic, data)` works only while the app has exactly one namespace; with several it throws.
|
|
1513
|
-
|
|
1514
|
-
**Callbacks.** `handler`, then each inbound frame, then the `close` handler run in order, one at a time. Async callbacks are awaited. Frames that arrive before `conn.on("message")` wait for it. A throw or rejection goes to `onError`, or to the app logger when there is none; it never reaches the runtime. A failing `handler` closes the connection with 1011. On close, the connection leaves every topic before the `close` handler runs. After close, `join` and `send` do nothing, and `publish` still works, for "left" announcements.
|
|
1515
|
-
|
|
1516
|
-
`conn`:
|
|
1517
|
-
|
|
1518
|
-
| Field | Meaning |
|
|
1519
|
-
| ------------------------------ | ------------------------------------------ |
|
|
1520
|
-
| `id` | Connection id |
|
|
1521
|
-
| `userId` | What `identify(ctx)` returned, else `null` |
|
|
1522
|
-
| `transport` | Always `"ws"` |
|
|
1523
|
-
| `closed` | `true` once closed |
|
|
1524
|
-
| `state` | Mutable bag |
|
|
1525
|
-
| `join(topic)` / `leave(topic)` | Topic membership in this route's namespace |
|
|
1526
|
-
| `send(payload)` | One JSON frame to this connection |
|
|
1527
|
-
| `publish(topic, payload)` | One JSON frame to everyone on the topic |
|
|
1528
|
-
| `close(reason?)` | Close with 1000 (reason cut to 123 bytes) |
|
|
1529
|
-
| `on("message" \| "close", fn)` | Inbound frames / teardown |
|
|
1530
|
-
|
|
1531
|
-
`limits` (per connection):
|
|
1532
|
-
|
|
1533
|
-
| Option | Default | Past it |
|
|
1534
|
-
| ------------------ | ------- | ------------------------------------------------------------ |
|
|
1535
|
-
| `maxTopics` | 128 | `join` throws |
|
|
1536
|
-
| `maxFrameBytes` | 1 MiB | close 1009 |
|
|
1537
|
-
| `maxPendingFrames` | 64 | close 1008 (frames waiting for a slow or missing handler) |
|
|
1538
|
-
| `maxBufferedBytes` | 4 MiB | `slowConsumer`: `"close"` (1013, default) or `"drop"` frames |
|
|
1539
|
-
|
|
1540
|
-
`maxBufferedBytes` applies where the runtime reports how much it holds for a client (`bufferedAmount`). Cap inbound frame size at the adapter as well, because the runtime reads a frame before Honey sees it.
|
|
1541
|
-
|
|
1542
|
-
The `use` option adds middleware for this socket only. Duplicate `realtime()` paths throw, and so do the unsupported `reconnectBuffer` and `transports` options. The bus is in-process: it does not cross processes or Cloudflare isolates (REST publish to another connection is skipped on the CF e2e env).
|
|
1543
|
-
|
|
1544
|
-
## SSE and streaming
|
|
1545
|
-
|
|
1546
|
-
```ts
|
|
1547
|
-
app.get("/events").handler((ctx) =>
|
|
1548
|
-
ctx.res.sse(
|
|
1549
|
-
async (stream) => {
|
|
1550
|
-
if (stream.lastEventId) {
|
|
1551
|
-
await stream.send({ event: "resume", data: `from ${stream.lastEventId}` })
|
|
1552
|
-
}
|
|
1553
|
-
try {
|
|
1554
|
-
for (const update of await feed.since(stream.lastEventId, { signal: stream.signal })) {
|
|
1555
|
-
await stream.send({ event: "data", data: update, id: update.id })
|
|
1556
|
-
}
|
|
1557
|
-
} finally {
|
|
1558
|
-
stream.close()
|
|
1559
|
-
}
|
|
1560
|
-
},
|
|
1561
|
-
{ defaultRetry: 3000, keepalive: 15_000 },
|
|
1562
|
-
),
|
|
1563
|
-
)
|
|
1564
|
-
```
|
|
1565
|
-
|
|
1566
|
-
`Last-Event-Id` is read automatically. Event names and ids must not contain newlines. `keepalive` writes `: heartbeat` comments.
|
|
1567
|
-
|
|
1568
|
-
```ts
|
|
1569
|
-
app.get("/stream").handler((ctx) =>
|
|
1570
|
-
ctx.res.stream(async (writable, signal) => {
|
|
1571
|
-
const w = writable.getWriter()
|
|
1572
|
-
while (!signal.aborted) {
|
|
1573
|
-
await w.write(new TextEncoder().encode("chunk"))
|
|
1574
|
-
await sleep(1_000)
|
|
1575
|
-
}
|
|
1576
|
-
}),
|
|
1577
|
-
)
|
|
1578
|
-
|
|
1579
|
-
app.get("/gen").handler((ctx) =>
|
|
1580
|
-
ctx.res.generate(
|
|
1581
|
-
(async function* () {
|
|
1582
|
-
try {
|
|
1583
|
-
yield "one"
|
|
1584
|
-
yield "two"
|
|
1585
|
-
} finally {
|
|
1586
|
-
/* runs on a normal end, a disconnect, a timeout or shutdown */
|
|
1587
|
-
}
|
|
1588
|
-
})(),
|
|
1589
|
-
{ contentType: "text/plain", status: 200 },
|
|
1590
|
-
),
|
|
1591
|
-
)
|
|
1592
|
-
```
|
|
1593
|
-
|
|
1594
|
-
All three share one lifecycle:
|
|
1595
|
-
|
|
1596
|
-
- **Lazy start.** The callback (or generator) runs when the body is first read. A HEAD request, or middleware that replaces the response, never starts it.
|
|
1597
|
-
- **One end.** The stream ends when the producer finishes, when the client disconnects, or when `ctx.signal` aborts (`timeout()`, server shutdown). `stream.signal` / the `signal` argument abort with it, a generator gets `return()` so its `finally` runs, and the keepalive timer is cleared.
|
|
1598
|
-
- **Writes after the end.** `stream.send()` and `writer.write()` reject with an `AbortError`, which ends an awaiting loop. Honey does not report that rejection, and an unawaited send never becomes an unhandled rejection. `stream.close()` is synchronous and idempotent, so it is safe in a `finally`.
|
|
1599
|
-
- **Errors.** A callback or generator that throws breaks the body (the client sees an aborted transfer, not a clean end) and is logged through the app logger, or `console.error` without one.
|
|
1600
|
-
- **Flushing.** `ctx.res.stream()` ends the body after queued writes flush when the callback returns, whether or not you closed the writable.
|
|
1601
|
-
|
|
1602
|
-
`ctx.signal` is the same signal for any handler: it aborts when the client disconnects, when `timeout()` fires, or when the server shuts down. On Deno, `request.signal` also aborts once a response has been delivered (Deno's legacy behavior; it prints a notice the first time a handler listens), so treat an abort after the response as normal.
|
|
1603
|
-
|
|
1604
|
-
## Generated clients
|
|
1605
|
-
|
|
1606
|
-
One OpenAPI document, four printers. Parity: typed operations, typed errors, `onAuthExpired` + one 401 retry, cancellation, per-call timeout/headers, request/response hooks, invalidation, SSE, realtime, WebSocket, streaming bodies, `onLog`. Python and Rust also emit a sync runtime.
|
|
1607
|
-
|
|
1608
|
-
### Plugin codegen config
|
|
1609
|
-
|
|
1610
|
-
See [honey generate](#honey-generate). `codegen.sdk: true` is TypeScript-only into `src/_gen`. Object form selects ports. `codegen.sdk.specs: ["a.json"]` builds from existing specs (no app required for SDK). `codegen.cli=true` is invalid — pass `{ out, binaryName }`.
|
|
1611
|
-
|
|
1612
|
-
### TypeScript `createClient`
|
|
1613
|
-
|
|
1614
|
-
Hand-typed client against `InferRoutes<typeof app>` (no generate):
|
|
1615
|
-
|
|
1616
|
-
<!-- snippet:define client-app
|
|
1617
|
-
```ts
|
|
1618
|
-
import { honey } from "@lovrozagar/honey"
|
|
1619
|
-
import * as z from "zod"
|
|
1620
|
-
|
|
1621
|
-
const root = honey()
|
|
1622
|
-
root.ws("/echo-ws").handler({ onMessage: (_ctx, ws, data) => ws.send(data) })
|
|
1623
|
-
const app = root
|
|
1624
|
-
.basePath("/api")
|
|
1625
|
-
.get("/health")
|
|
1626
|
-
.handler((ctx) => ctx.res.json("ok", { status: "ok" }))
|
|
1627
|
-
.get("/users/:id")
|
|
1628
|
-
.handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
|
|
1629
|
-
.post("/users")
|
|
1630
|
-
.input({ json: z.object({ email: z.string(), name: z.string() }) })
|
|
1631
|
-
.output({ "application/json": { created: z.object({ id: z.string() }) } })
|
|
1632
|
-
.handler((ctx) => ctx.res.json("created", { id: "u-1" }))
|
|
1633
|
-
```
|
|
1634
|
-
-->
|
|
1635
|
-
<!-- snippet:continue client-app -->
|
|
1636
|
-
|
|
1637
|
-
```ts
|
|
1638
|
-
import { createClient, isClientError } from "@lovrozagar/honey/client"
|
|
1639
|
-
|
|
1640
|
-
const client = createClient<typeof app>({
|
|
1641
|
-
baseURL: "http://127.0.0.1:3000",
|
|
1642
|
-
headers: { authorization: "Bearer t" },
|
|
1643
|
-
timeout: 10_000,
|
|
1644
|
-
throwOnError: false,
|
|
1645
|
-
credentials: "include",
|
|
1646
|
-
onAuthExpired: async () => "new-token",
|
|
1647
|
-
onRequest: [
|
|
1648
|
-
async (ctx) => {
|
|
1649
|
-
ctx.headers.set("x-trace", "1")
|
|
1650
|
-
},
|
|
1651
|
-
],
|
|
1652
|
-
onResponse: [async (ctx) => ctx.response],
|
|
1653
|
-
})
|
|
1654
|
-
|
|
1655
|
-
/* Browser, same-origin: a path is resolved against location.origin.
|
|
1656
|
-
Omit fetch — the default is the environment fetch, bound for window. */
|
|
1657
|
-
const sameOrigin = createClient<typeof app>({
|
|
1658
|
-
baseURL: "/api",
|
|
1659
|
-
credentials: "include",
|
|
1660
|
-
})
|
|
1661
|
-
|
|
1662
|
-
const res = await client.get("/api/health")
|
|
1663
|
-
const created = await client.post("/api/users", { json: { email: "a@b.com", name: "Ada" } })
|
|
1664
|
-
|
|
1665
|
-
client.$url("/api/users/:id", { params: { id: "1" }, search: { x: "1" } })
|
|
1666
|
-
client.$path("/api/users/:id", { params: { id: "1" } })
|
|
1667
|
-
const ws = client.ws("/echo-ws", { reconnectToken: "t" })
|
|
1668
|
-
```
|
|
1669
|
-
|
|
1670
|
-
`baseURL` may be an absolute `http(s):` / `ws(s):` URL or a same-origin path (`"/api"`). Paths resolve against `location.origin`. Omit `fetch` to use the environment `fetch` (bound for browsers). In Node and Workers without an origin, pass an absolute URL.
|
|
1671
|
-
|
|
1672
|
-
Per-call options: `json`, `form`, `search`, `params`, `headers`, `cookies`, `timeout`, `signal`, `lastEventId`.
|
|
1673
|
-
|
|
1674
|
-
More config:
|
|
1675
|
-
|
|
1676
|
-
- `onAuthExpired({ rejectedToken })` runs on a 401 and returns the new token; the request is retried once with it (not for stream bodies). The token goes in `authHeaderName` (default `Authorization`) with `authHeaderPrefix` (default `"Bearer "`).
|
|
1677
|
-
- `redirect` — redirects are followed only to the same origin by default, so custom auth headers never follow a cross-origin redirect.
|
|
1678
|
-
- `requestId: false` stops the client from sending `x-request-id` (which makes every cross-origin request preflighted).
|
|
1679
|
-
- A path param that is `""`, `.` or `..` throws `PathParamError` before anything is sent.
|
|
1680
|
-
|
|
1681
|
-
`throwOnError: true` throws `ClientError` subclasses (`BadRequestError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, … `GatewayTimeoutError`). `isClientError(e)` is the guard. In safe mode, `error` is set (truthy) for every non-2xx, even when the body is empty or not JSON.
|
|
1682
|
-
|
|
1683
|
-
Awaiting a call to an SSE route returns an async iterable of events: `for await (const ev of await client.get("/events"))`. On `client.ws(...)`, text frames go to `"message"` and binary frames to `"binary"` (an `ArrayBuffer`).
|
|
1684
|
-
|
|
1685
|
-
### Generated SDK usage
|
|
1686
|
-
|
|
1687
|
-
After `honey generate` with `codegen.sdk.ports.typescript`:
|
|
1688
|
-
|
|
1689
|
-
```ts
|
|
1690
|
-
import { MySDK, isClientError, UnauthorizedError } from "./_gen/sdk.index.gen.ts"
|
|
1691
|
-
|
|
1692
|
-
const sdk = new MySDK({
|
|
1693
|
-
baseURL: "http://127.0.0.1:3000",
|
|
1694
|
-
headers: { Authorization: "Bearer t" },
|
|
1695
|
-
onAuthExpired: () => Promise.resolve("new-token"),
|
|
1696
|
-
onLog: (entry) => console.debug(entry.event, entry.operation, entry.duration_ms),
|
|
1697
|
-
onRequest: [
|
|
1698
|
-
(ctx) => {
|
|
1699
|
-
ctx.headers["X-Trace-Id"] = crypto.randomUUID()
|
|
1700
|
-
},
|
|
1701
|
-
],
|
|
1702
|
-
throwOnError: true,
|
|
1703
|
-
timeout: 10_000,
|
|
1704
|
-
invalidation: { staleTime: 5 },
|
|
1705
|
-
})
|
|
1706
|
-
|
|
1707
|
-
const browserSdk = new MySDK({
|
|
1708
|
-
baseURL: "/api",
|
|
1709
|
-
credentials: "include",
|
|
1710
|
-
throwOnError: true,
|
|
1711
|
-
})
|
|
1712
|
-
|
|
1713
|
-
const user = await sdk.createUser({ json: { email: "a@b.com", name: "Ada" } })
|
|
1714
|
-
```
|
|
1715
|
-
|
|
1716
|
-
- Method names come from `operationId`; an operation without one gets a derived name from its method and path (`getUsersById`). One `operationId` that expands to several methods or optional-param variants gets a method or variant suffix.
|
|
1717
|
-
- A resource name that would shadow a client member or an `Object.prototype` key (`state`, `dispose`, `then`, `toString`) gets a trailing `_` (`sdk.state_`). Schemas named like SDK or language types get a `Model` suffix in Go, Rust and Python (`Config` → `ConfigModel`).
|
|
1718
|
-
- A path param that is `""`, `.` or `..` is refused before sending. Redirects are followed only to the same origin by default (`redirect` option). In safe mode, `error` is truthy for every non-2xx.
|
|
1719
|
-
- The same request rules hold in every language (base path and query kept, `URLSearchParams` query encoding, one auth refresh for concurrent 401s, no retry of streamed bodies); a shared conformance suite runs them against each generated SDK.
|
|
1720
|
-
|
|
1721
|
-
Python / Go / Rust: point `ports.*.outDir` at a folder and import that package (`replace` in `go.mod`, `path =` in Cargo). Sync clients exist for Python and Rust only.
|
|
1722
|
-
|
|
1723
|
-
Capability details and four-language snippets: [SDK index](./docs/sdk.md) and [examples](https://github.com/lovrozagar/honey/tree/main/packages/core/examples).
|
|
1724
|
-
|
|
1725
|
-
### Go CLI
|
|
1726
|
-
|
|
1727
|
-
<!-- snippet:skip -->
|
|
1728
|
-
|
|
1729
|
-
```ts
|
|
1730
|
-
codegen: {
|
|
1731
|
-
cli: {
|
|
1732
|
-
out: "cli",
|
|
1733
|
-
binaryName: "myapi",
|
|
1734
|
-
modulePath: "example.com/myapi/cli",
|
|
1735
|
-
sdkModulePath: "example.com/myapi", // omit to embed the SDK
|
|
1736
|
-
defaultBaseURL: "http://127.0.0.1:3000",
|
|
1737
|
-
envPrefix: "MYAPI",
|
|
1738
|
-
configName: "myapi",
|
|
1739
|
-
},
|
|
1740
|
-
}
|
|
1741
|
-
```
|
|
1742
|
-
|
|
1743
|
-
Equivalent CLI: `honey generate --cli --cli-out cli --cli-binary-name myapi`.
|
|
1744
|
-
|
|
1745
|
-
### Programmatic codegen
|
|
1746
|
-
|
|
1747
|
-
```ts
|
|
1748
|
-
import {
|
|
1749
|
-
generateOpenApi,
|
|
1750
|
-
generateManifest,
|
|
1751
|
-
generateSDK,
|
|
1752
|
-
generateRouteTreeFromApp,
|
|
1753
|
-
mergeSpecs,
|
|
1754
|
-
sanitizeOpenApiSpec,
|
|
1755
|
-
} from "@lovrozagar/honey/codegen"
|
|
1756
|
-
import { generateGoCLI } from "@lovrozagar/honey/codegen-go-cli"
|
|
1757
|
-
|
|
1758
|
-
const spec = await generateOpenApi(app, { info: { title: "API", version: "1" } })
|
|
1759
|
-
const { files } = generateSDK(spec, { name: "MySDK", stem: "sdk" })
|
|
1760
|
-
```
|
|
1761
|
-
|
|
1762
|
-
Language printers used by the plugin (`generatePythonSDK`, `generateGoSDK`, `generateRustSDK`) are not separate package exports — call `honey generate`.
|
|
1763
|
-
|
|
1764
|
-
## Type inference
|
|
1765
|
-
|
|
1766
|
-
Exported from `@lovrozagar/honey`. They describe the **app you built**:
|
|
1767
|
-
|
|
1768
|
-
| Type | Meaning |
|
|
1769
|
-
| ---------------------------------------------------------------------------- | ------------------------------------ |
|
|
1770
|
-
| `InferRoutes<typeof app>` | Path → methods → input/output/errors |
|
|
1771
|
-
| `InferRoutePaths<typeof app>` | Union of paths |
|
|
1772
|
-
| `InferRouteMethods<typeof app, Path>` | Methods on one path |
|
|
1773
|
-
| `InferRouteInput<typeof app, Path, Method>` | Validated input |
|
|
1774
|
-
| `InferRouteOutput<typeof app, Path, Method>` | Output map |
|
|
1775
|
-
| `InferRouteErrors<typeof app, Path, Method>` | Declared error keys |
|
|
1776
|
-
| `InferRouteMeta<typeof app, Path, Method>` | Route meta |
|
|
1777
|
-
| `InferRouteCtx<typeof app, Path, Method>` | Handler ctx for one route |
|
|
1778
|
-
| `InferCtx<typeof app>` | Handler context (no `res` brand) |
|
|
1779
|
-
| `InferEnv<typeof app>` | `ctx.env` |
|
|
1780
|
-
| `InferMeta<typeof app>` | App-level meta |
|
|
1781
|
-
| `InferErrorFactory<typeof app>` | Error factory |
|
|
1782
|
-
| `InferBasePath<typeof app>` | Base path string |
|
|
1783
|
-
| `StatusKey` / `SuccessStatusKey` | Status-key unions |
|
|
1784
|
-
| `HoneyCtx<TEnv>` | Untyped context shape |
|
|
1785
|
-
| `HoneyServeOptions` / `ServeHandle` / `ServeRuntime` | Serve types |
|
|
1786
|
-
| `WSHandler` / `WSContext` / `WSAdapter` | Socket types |
|
|
1787
|
-
| `ConnContext` / `RealtimeRouteOpts` / `RealtimeLimits` / `RealtimePublisher` | Realtime types |
|
|
1788
|
-
|
|
1789
|
-
<!-- snippet:continue client-app -->
|
|
1790
|
-
|
|
1791
|
-
```ts
|
|
1792
|
-
import type { InferCtx, InferRouteInput, InferRoutes } from "@lovrozagar/honey"
|
|
1793
|
-
|
|
1794
|
-
type AppRoutes = InferRoutes<typeof app>
|
|
1795
|
-
type CreateInput = InferRouteInput<typeof app, "/api/users", "post">
|
|
1796
|
-
type Ctx = InferCtx<typeof app>
|
|
1797
|
-
```
|
|
1798
|
-
|
|
1799
|
-
## Testing
|
|
1800
|
-
|
|
1801
|
-
```ts
|
|
1802
|
-
import { testClient } from "@lovrozagar/honey/testing"
|
|
1803
|
-
|
|
1804
|
-
const client = testClient(app, { env: { DATABASE_URL: "…" }, cookies: true })
|
|
1805
|
-
|
|
1806
|
-
const res = await client.get("/api/health")
|
|
1807
|
-
await client.post("/api/users", {
|
|
1808
|
-
json: { email: "a@b.com", name: "Ada" },
|
|
1809
|
-
headers: { authorization: "Bearer t" },
|
|
1810
|
-
search: { dry: "1" },
|
|
1811
|
-
})
|
|
1812
|
-
await client.post("/login", { form: { user: "a", pass: "b" } })
|
|
1813
|
-
```
|
|
1814
|
-
|
|
1815
|
-
Methods: `get` `post` `put` `patch` `delete` `head` `options` `request`. `cookies: true` stores `Set-Cookie` and sends them back; a cookie set with `Max-Age=0` (or a past `Expires`) is removed.
|
|
1816
|
-
|
|
1817
|
-
`transport: "node"` serves the app with honey's Node adapter on a random local port and sends real HTTP, so the request goes through the Node `Request` view and response writer, as in production on Node. Call `await client.close()` when done.
|
|
1818
|
-
|
|
1819
|
-
## Utilities
|
|
1820
|
-
|
|
1821
|
-
```ts
|
|
1822
|
-
import { accepts } from "@lovrozagar/honey/accepts"
|
|
1823
|
-
accepts(req, ["application/json", "text/html"]) // best match or first if no Accept
|
|
1824
|
-
|
|
1825
|
-
import { serializeCookie } from "@lovrozagar/honey/cookie"
|
|
1826
|
-
serializeCookie("sid", { value: "abc", httpOnly: true, path: "/" })
|
|
1827
|
-
|
|
1828
|
-
import { sign, verify } from "@lovrozagar/honey/cookie-sign"
|
|
1829
|
-
const signed = await sign("abc", SECRET, { name: "sid" }) // binds the cookie name
|
|
1830
|
-
const raw = await verify(signed, [SECRET, OLD_SECRET], { name: "sid" }) // null if none match or malformed
|
|
1831
|
-
// `legacy: false` stops accepting signatures from earlier releases, which were not bound to a name
|
|
1832
|
-
|
|
1833
|
-
import { timingSafeEqual } from "@lovrozagar/honey/crypto"
|
|
1834
|
-
await timingSafeEqual(a, b)
|
|
1835
|
-
|
|
1836
|
-
import { requestToCurl } from "@lovrozagar/honey/request-to-curl"
|
|
1837
|
-
await requestToCurl(req, { excludeHeader: (n) => n === "authorization" })
|
|
1838
|
-
```
|
|
1839
|
-
|
|
1840
|
-
`honey/openapi/spec` walks a live tree without pulling `codegen.ts`. Intern trees omit JSON Schema — serve generate-time OpenAPI JSON as Worker assets. `honey/openapi/scalar` and `honey/openapi/swagger` are the docs UIs `app.openapi()` loads. `honey/cli` is the generate/init binary. `honey/codegen/extract` is the ts-morph extractor used by `--types`.
|
|
1841
|
-
|
|
1842
|
-
## Package exports
|
|
1843
|
-
|
|
1844
|
-
Import features from their path.
|
|
1845
|
-
|
|
1846
|
-
| Export | Purpose |
|
|
1847
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
|
|
1848
|
-
| `@lovrozagar/honey` | `honey`, `defineErrors`, `HoneyError`, `createMiddleware`, `HoneyRes`, Infer* types, `mergeTree`, `detectRuntime` |
|
|
1849
|
-
| `honey/serve` | Register Node/Bun/Deno listen |
|
|
1850
|
-
| `honey/node` | Low-level Node `serve()` |
|
|
1851
|
-
| `honey/openapi` | Register live spec generation (pulls codegen) |
|
|
1852
|
-
| `honey/openapi/spec` | Runtime spec walker — live tree, no codegen. Intern omits JSON Schema |
|
|
1853
|
-
| `honey/openapi/scalar` `honey/openapi/swagger` | Docs UI |
|
|
1854
|
-
| `honey/i18n` | Register error i18n |
|
|
1855
|
-
| `@lovrozagar/honey/plugin` | Vite plugin + `generateFromApp` |
|
|
1856
|
-
| `honey/client` / `honey/client/sdk` | Typed TS client runtime / generated-SDK helpers |
|
|
1857
|
-
| `honey/cors` `honey/csrf` `honey/body-limit` `honey/logger` `honey/curl-logger` `honey/etag` `honey/timeout` `honey/request-id` `honey/secure-headers` `honey/server-timing` `honey/ip-restrict` `honey/powered-by` `honey/pretty-json` `honey/static` | Middleware |
|
|
1858
|
-
| `honey/proxy` | Registers `.proxy()` on route builders (import once); `ProxyConfig` type |
|
|
1859
|
-
| `honey/realtime` | Registers `app.realtime()` (import once) |
|
|
1860
|
-
| `honey/trust` | Registers hop counts and address lists for `app.trustProxy()` (import once) |
|
|
1861
|
-
| `honey/input` | `readableStream()` |
|
|
1862
|
-
| `honey/testing` | `testClient` |
|
|
1863
|
-
| `honey/ws/bun` `honey/ws/node` `honey/ws/deno` `honey/ws/cloudflare` | WS adapters |
|
|
1864
|
-
| `honey/ws/session` | The websocket session runtime. Every honey adapter imports it; a custom `WSAdapter` in a bundled build imports it once |
|
|
1865
|
-
| `honey/accepts` `honey/cookie` `honey/cookie-sign` `honey/crypto` `honey/request-to-curl` | Utilities |
|
|
1866
|
-
| `honey/telemetry/otel` | `otelAdapter` |
|
|
1867
|
-
| `honey/codegen` | `generateOpenApi`, `generateSDK`, `generateManifest`, trees, sanitize |
|
|
1868
|
-
| `honey/codegen-go-cli` | `generateGoCLI` |
|
|
1869
|
-
| `honey/codegen/extract` | ts-morph chain extractor |
|
|
1870
|
-
| `honey/build` | `createBuildPlugin` |
|
|
1871
|
-
| `honey/tree` | Tree types + `mergeTree` |
|
|
1872
|
-
| `honey/cli` | `honey` binary |
|
|
1873
|
-
| `honey/errors` | `defineErrors` (also on the root export) |
|
|
1874
|
-
|
|
1875
|
-
## Repository layout
|
|
1876
|
-
|
|
1877
|
-
```
|
|
1878
|
-
packages/core/ published `honey` package (src, tests, docs, examples)
|
|
1879
|
-
e2e/apps/ consumer apps (kitchen, defaults, compose, surface, gateway)
|
|
1880
|
-
e2e/{bun,node,deno,cf-workers}/ runtime hosts + Playwright
|
|
1881
|
-
e2e/run.ts env × app runner
|
|
1882
|
-
bench/ bombardier vs Hono / Elysia / Express / Nest (bun + node)
|
|
1883
|
-
```
|
|
1884
|
-
|
|
1885
|
-
`e2e/apps/*` own the tests. Runtimes only listen. `HONEY_E2E_APP` and `HONEY_E2E_ENV` select which app and host.
|
|
1886
|
-
|
|
1887
|
-
## Develop
|
|
1888
|
-
|
|
1889
|
-
`packages/core` is the published package. `e2e/*` imports `honey` over `workspace:*`.
|
|
1890
|
-
|
|
1891
|
-
Requires [Bun](https://bun.sh) 1.3+.
|
|
1892
|
-
|
|
1893
|
-
```bash
|
|
1894
|
-
bun install
|
|
1895
|
-
bun run generate # honey generate for every e2e app
|
|
1896
|
-
bun run test # core unit + in-process integration (default CI)
|
|
1897
|
-
bun run test:consumers # e2e apps import honey like a real app
|
|
1898
|
-
bun run test:e2e # Playwright, bun × every e2e app
|
|
1899
|
-
bun run test:e2e:node # same matrix against Node (tsx)
|
|
1900
|
-
bun run test:e2e:deno # same matrix against Deno
|
|
1901
|
-
bun run test:e2e:cf # same matrix against local wrangler / workerd
|
|
1902
|
-
bun run test:e2e:all # bun + node + deno + local cf × every e2e app
|
|
1903
|
-
bun run typecheck # core src (TypeScript 7)
|
|
1904
|
-
bun run typecheck:consumers # every e2e app + generated types
|
|
1905
|
-
bun run lint # oxlint
|
|
1906
|
-
bun run lint:fix # oxlint --fix
|
|
1907
|
-
bun run fmt # oxfmt
|
|
1908
|
-
bun run fmt:check # oxfmt --check
|
|
1909
|
-
```
|
|
1910
|
-
|
|
1911
|
-
Opt-in locally (CI `harness` job runs the first one):
|
|
1912
|
-
|
|
1913
|
-
```bash
|
|
1914
|
-
bun run test:harness # TS / Go / Python / Rust / MCP compile + behavioral
|
|
1915
|
-
bun run test:harness:rust # rust-only subset of the same loop
|
|
1916
|
-
bun run test:all # default suite + language harnesses
|
|
1917
|
-
```
|
|
1918
|
-
|
|
1919
|
-
The harness needs Go, cargo and `python3` on PATH. Python packages come from
|
|
1920
|
-
`packages/core/tests/python-requirements.txt`: on first run the harness creates
|
|
1921
|
-
`.cache/python-venv` and installs them there, then reuses it until the file changes.
|
|
1922
|
-
Set `HONEY_PYTHON` to use your own interpreter instead. Without a usable Python
|
|
1923
|
-
(no `python3`, no `venv` module, or offline on first run) the Python tests skip
|
|
1924
|
-
with a warning; `HONEY_REQUIRE_PYTHON=1` (set in CI) turns that into a failure.
|
|
1925
|
-
|
|
1926
|
-
Typecheck stays strict. Do not weaken `strict` or add `as any` to make it pass.
|
|
1927
|
-
|
|
1928
|
-
### Test matrix
|
|
1929
|
-
|
|
1930
|
-
| Command | What it proves |
|
|
1931
|
-
| ------------------------ | -------------------------------------------------------------------------------- |
|
|
1932
|
-
| `bun run test` | Core unit + integration. Default CI gate. |
|
|
1933
|
-
| `bun run test:consumers` | Each e2e app imports `honey` and hits a few routes in-process. |
|
|
1934
|
-
| `bun run test:e2e` | Playwright against Bun listen, every app. |
|
|
1935
|
-
| `bun run test:e2e:node` | Same tests, Node (`tsx`) listen. |
|
|
1936
|
-
| `bun run test:e2e:deno` | Same tests, Deno listen. |
|
|
1937
|
-
| `bun run test:e2e:cf` | Same tests, local workerd. Kitchen REST publish is skipped (`HONEY_E2E_ENV=cf`). |
|
|
1938
|
-
| `bun run test:build` | Every app builds for every deploy target; artifacts and size budgets hold. |
|
|
1939
|
-
| `bun run test:harness` | Generated SDKs compile and behave. |
|
|
1940
|
-
|
|
1941
|
-
```bash
|
|
1942
|
-
bun e2e/run.ts --env node --app kitchen
|
|
1943
|
-
bun e2e/run.ts --env all --app surface
|
|
1944
|
-
bun e2e/run.ts --env bun --mode prod
|
|
1945
|
-
|
|
1946
|
-
bun e2e/run-build.ts --app kitchen --target cloudflare
|
|
1947
|
-
bun e2e/run-build.ts --runtime all # run the builds under node and bun
|
|
1948
|
-
bun e2e/run-build.ts --check-generated # CI: a build must not rewrite committed files
|
|
1949
|
-
```
|
|
1950
|
-
|
|
1951
|
-
The build tier builds each e2e app through its own `vite.config.ts` plus
|
|
1952
|
-
`createBuildPlugin`, once per target (`node`, `bun`, `deno`, `cloudflare`). A cell
|
|
1953
|
-
passes when the build exits 0, emits `dist/<target>/index.js`, and stays under its
|
|
1954
|
-
gzipped size budget. The budget is not decoration: honey loads `zod` and `effect`
|
|
1955
|
-
dynamically, and a bundler that resolves one of those dynamic imports silently adds
|
|
1956
|
-
~300KB to every worker bundle. Size is the only signal that catches it.
|
|
1957
|
-
|
|
1958
|
-
### E2E apps and runtimes
|
|
1959
|
-
|
|
1960
|
-
| App | Covers |
|
|
1961
|
-
| ---------- | -------------------------------------------------------------------- |
|
|
1962
|
-
| `kitchen` | Auth, CRUD, i18n, OpenAPI, SSE, WS, realtime, trailing slash, errors |
|
|
1963
|
-
| `defaults` | Empty-middleware app, root OpenAPI, no CORS by default |
|
|
1964
|
-
| `compose` | `.route()` groups, Scalar collision with a user `/docs` |
|
|
1965
|
-
| `surface` | Every input source, output type, method, SSE, WS, uploads |
|
|
1966
|
-
| `gateway` | `stripPrefix` + enforce slash, Swagger behind a prefix |
|
|
1967
|
-
|
|
1968
|
-
Runtimes: `e2e/bun`, `e2e/node`, `e2e/deno`, `e2e/cf-workers`.
|
|
1969
|
-
|
|
1970
|
-
### Live Cloudflare proof
|
|
1971
|
-
|
|
1972
|
-
Same kitchen app on workerd: https://honey-cf-e2e.lovro-zagar5.workers.dev
|
|
1973
|
-
|
|
1974
|
-
See `e2e/cf-workers/README.md`. Redeploy with `bun run deploy:e2e:cf`. Live soak: `bun run test:live:cf`.
|
|
1975
|
-
|
|
1976
|
-
### Bench
|
|
1977
|
-
|
|
1978
|
-
`bench/` compares Honey, Hono, Elysia, Express, and Nest on Bun and Node (naked + Zod). Bombardier, 10s / 100 connections, localhost.
|
|
1979
|
-
|
|
1980
|
-
```bash
|
|
1981
|
-
bun run --filter @honey/bench build:all
|
|
1982
|
-
bun run --filter @honey/bench bench:bun
|
|
1983
|
-
bun run --filter @honey/bench bench:node
|
|
1984
|
-
```
|
|
1985
|
-
|
|
1986
|
-
Numbers and bundle sizes: [`bench/RESULTS.md`](https://github.com/lovrozagar/honey/blob/main/bench/RESULTS.md).
|
|
1987
|
-
|
|
1988
|
-
`bun run bench:hotpath` is the hot-path regression gate CI runs: in-process `app.fetch` cost on static, param, chain-middleware, scoped-middleware and 404 paths, as a ratio to a hand-written fetch handler in the same process, against `bench/hotpath-baseline.json` (`--update` records a new baseline). It fails when a ratio grows past `HOTPATH_THRESHOLD` (default 25%). Baselines are kept per Bun minor version.
|
|
1989
|
-
|
|
1990
|
-
Python runtime tests skip without `httpx` (`pip install httpx`). Rust cargo tests skip without `cargo` or when `HONEY_RUST_INTEGRATION=0`. Go tests skip without `go`. Cargo artifacts go to `.cache/cargo-target`, not `/tmp`.
|
|
1991
|
-
|
|
1992
|
-
## Releases
|
|
54
|
+
`npx honey generate` writes the app's route tree, manifest, OpenAPI JSON and YAML and its types beside it.
|
|
1993
55
|
|
|
1994
|
-
|
|
56
|
+
## Documentation
|
|
1995
57
|
|
|
1996
|
-
|
|
58
|
+
- [The docs](https://github.com/lovrozagar/honey/blob/main/docs/README.md): routing, input and output, middleware,
|
|
59
|
+
streaming, WebSockets and realtime, the OpenAPI document, testing, and every capability new in 1.0.
|
|
60
|
+
- [Generated SDKs, the CLI and MCP](https://github.com/lovrozagar/honey/blob/main/docs/codegen.md), and their
|
|
61
|
+
[per-language recipes](https://github.com/lovrozagar/honey/blob/main/docs/sdk-recipes.md).
|
|
62
|
+
- [Every package export](https://github.com/lovrozagar/honey/blob/main/docs/packages.md).
|
|
63
|
+
- [Migrating from 0.7](https://github.com/lovrozagar/honey/blob/main/docs/migration.md).
|
|
1997
64
|
|
|
1998
65
|
## License
|
|
1999
66
|
|