@lovrozagar/honey 0.7.3 → 1.0.0-rc.3
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 +930 -1664
- 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 +175 -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/meta-types.d.ts +34 -0
- package/dist/routing/meta-types.js +5 -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 +136 -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,1265 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lovrozagar/honey
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A typed web framework for the Fetch API whose routes are the source of its clients. You write a route once, with its
|
|
4
|
+
input and output schemas; honey serves it on Node, Bun or Cloudflare Workers, and
|
|
5
|
+
[`@lovrozagar/honey-clients`](https://www.npmjs.com/package/@lovrozagar/honey-clients) turns the same routes into an
|
|
6
|
+
OpenAPI 3.1 document, typed SDKs for six languages, a command-line client and an MCP server.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Nothing is inferred at runtime: the schemas you declare are what the handler receives, what the document says, and
|
|
9
|
+
what every generated client's types promise.
|
|
6
10
|
|
|
7
|
-
This
|
|
11
|
+
This page is the whole framework. Every TypeScript block below is type-checked by the repository's tests against this
|
|
12
|
+
package, so nothing here is pseudocode.
|
|
8
13
|
|
|
9
|
-
## Table of contents
|
|
10
|
-
|
|
11
|
-
- [Start](#start)
|
|
12
|
-
- [What Honey is](#what-honey-is)
|
|
13
14
|
- [Install](#install)
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
15
|
+
- [A route, end to end](#a-route-end-to-end)
|
|
16
|
+
- [The app](#the-app)
|
|
17
|
+
- [Routing](#routing)
|
|
18
|
+
- [Input and output](#input-and-output)
|
|
19
|
+
- [Responses](#responses)
|
|
20
|
+
- [Errors](#errors)
|
|
21
|
+
- [Middleware](#middleware)
|
|
22
|
+
- [The request context](#the-request-context)
|
|
23
|
+
- [Cookies](#cookies)
|
|
24
|
+
- [Streaming, WebSockets and realtime](#streaming-websockets-and-realtime)
|
|
25
|
+
- [Serving on Node, Bun and Cloudflare Workers](#serving-on-node-bun-and-cloudflare-workers)
|
|
26
|
+
- [Static files and proxying](#static-files-and-proxying)
|
|
27
|
+
- [The OpenAPI document](#the-openapi-document)
|
|
28
|
+
- [The MCP server](#the-mcp-server)
|
|
29
|
+
- [Logging and tracing](#logging-and-tracing)
|
|
30
|
+
- [Internationalization and crypto](#internationalization-and-crypto)
|
|
52
31
|
- [Testing](#testing)
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
55
|
-
- [
|
|
56
|
-
- [
|
|
57
|
-
|
|
58
|
-
- [E2E apps and runtimes](#e2e-apps-and-runtimes)
|
|
59
|
-
- [Live Cloudflare proof](#live-cloudflare-proof)
|
|
60
|
-
- [Bench](#bench)
|
|
61
|
-
- [Releases](#releases)
|
|
32
|
+
- [The build plugin and the honey CLI](#the-build-plugin-and-the-honey-cli)
|
|
33
|
+
- [Generated clients](#generated-clients)
|
|
34
|
+
- [Every export](#every-export)
|
|
35
|
+
- [Type helpers](#type-helpers)
|
|
36
|
+
- [Documentation](#documentation)
|
|
62
37
|
- [License](#license)
|
|
63
38
|
|
|
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).
|
|
115
|
-
|
|
116
39
|
## Install
|
|
117
40
|
|
|
118
|
-
|
|
41
|
+
1.0 is out as a release candidate, published under npm's `next` tag, so an install without a tag still gives 0.7.3.
|
|
42
|
+
Add `@next` until the final release:
|
|
119
43
|
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
npm add @lovrozagar/honey
|
|
124
|
-
# or
|
|
125
|
-
pnpm add @lovrozagar/honey
|
|
44
|
+
```sh
|
|
45
|
+
npm install @lovrozagar/honey@next
|
|
46
|
+
npx honey init
|
|
126
47
|
```
|
|
127
48
|
|
|
128
|
-
|
|
49
|
+
Node 22.12 or later, Bun 1.3 or later, or Cloudflare Workers. A schema library is yours to pick: honey validates
|
|
50
|
+
through [Standard Schema](https://standardschema.dev), so Zod, Valibot, ArkType, Effect Schema and Yup all work. The
|
|
51
|
+
examples here use Zod.
|
|
129
52
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
honey generate
|
|
133
|
-
```
|
|
53
|
+
`honey init` writes `src/app.ts`, `src/server.ts` and a Vite config; `honey init --cf` adds a Worker entry and a
|
|
54
|
+
`wrangler.jsonc`.
|
|
134
55
|
|
|
135
|
-
|
|
56
|
+
## A route, end to end
|
|
136
57
|
|
|
137
|
-
|
|
138
|
-
|
|
58
|
+
Declare what a route takes and what it answers. `ctx.input` is typed by the schemas, and so is `ctx.res.json`'s
|
|
59
|
+
payload for the status key you name:
|
|
139
60
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
```
|
|
61
|
+
```ts
|
|
62
|
+
import { defineErrors, honey } from "@lovrozagar/honey"
|
|
63
|
+
import * as z from "zod"
|
|
151
64
|
|
|
152
|
-
|
|
65
|
+
const errors = defineErrors({ not_found: "not_found" })
|
|
153
66
|
|
|
154
|
-
|
|
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 |
|
|
67
|
+
const app = honey()
|
|
159
68
|
|
|
160
|
-
|
|
69
|
+
app
|
|
70
|
+
.get("/books/:id")
|
|
71
|
+
.input({ params: z.object({ id: z.string() }), search: z.object({ lang: z.string().optional() }) })
|
|
72
|
+
.output({ "application/json": { ok: z.object({ id: z.string(), title: z.string() }) } })
|
|
73
|
+
.errors(errors, "not_found")
|
|
74
|
+
.handler((ctx) => {
|
|
75
|
+
if (ctx.input.params.id === "0") throw errors.not_found()
|
|
76
|
+
return ctx.res.json("ok", { id: ctx.input.params.id, title: "Dune" })
|
|
77
|
+
})
|
|
161
78
|
|
|
162
|
-
|
|
79
|
+
app
|
|
80
|
+
.post("/books")
|
|
81
|
+
.input({ json: z.object({ title: z.string().min(1) }) })
|
|
82
|
+
.handler((ctx) => ctx.res.json("created", { id: "b-1", title: ctx.input.json.title }))
|
|
163
83
|
|
|
164
|
-
|
|
165
|
-
honey init --cf
|
|
84
|
+
export default app
|
|
166
85
|
```
|
|
167
86
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Minimal app without the scaffold:
|
|
87
|
+
Serving is the same for any app, and the reference UI comes from the document the routes already describe:
|
|
171
88
|
|
|
172
89
|
```ts
|
|
173
90
|
import { honey } from "@lovrozagar/honey"
|
|
174
|
-
import
|
|
91
|
+
import { serveOpenApi } from "@lovrozagar/honey/openapi"
|
|
92
|
+
import { serve } from "@lovrozagar/honey/serve"
|
|
175
93
|
|
|
176
|
-
const app = honey
|
|
177
|
-
|
|
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
|
|
94
|
+
const app = honey()
|
|
95
|
+
app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
|
|
190
96
|
|
|
97
|
+
await serve(serveOpenApi(app, { docs: "scalar", title: "Books", version: "1.0.0" }), { port: 3000 })
|
|
191
98
|
```
|
|
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.
|
|
210
|
-
|
|
211
|
-
If there is no Vite config, you must pass `--app`.
|
|
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
99
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
}
|
|
100
|
+
```sh
|
|
101
|
+
curl http://127.0.0.1:3000/health
|
|
102
|
+
# ok
|
|
103
|
+
# the API reference: open http://127.0.0.1:3000/docs
|
|
280
104
|
```
|
|
281
105
|
|
|
282
|
-
|
|
106
|
+
Then generate a client from the app module itself, with no document to keep in step by hand:
|
|
283
107
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
`generateFromApp(app)` (from `@lovrozagar/honey/plugin`) is the in-process helper: it returns `{ routeTree, manifest?, openApi?, openApiYaml? }` without writing files.
|
|
288
|
-
|
|
289
|
-
## Builder
|
|
290
|
-
|
|
291
|
-
### Create the app
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
import { honey } from "@lovrozagar/honey"
|
|
295
|
-
|
|
296
|
-
const app = honey<{ DATABASE_URL: string; API_KEY: string }>()
|
|
108
|
+
```sh
|
|
109
|
+
npx honey-clients generate --app src/app.ts --target ts --out generated
|
|
297
110
|
```
|
|
298
111
|
|
|
299
|
-
The
|
|
112
|
+
## The app
|
|
300
113
|
|
|
301
|
-
|
|
114
|
+
`honey<TEnv>()` returns an app typed to its runtime environment — a Worker's bindings, or whatever your adapter
|
|
115
|
+
passes. The chain derives handles that share one settings graph: `basePath()` rebases every route registered through
|
|
116
|
+
it, `context()` adds static values the handler finds on `ctx`, and `use()` installs middleware for that handle and
|
|
117
|
+
its descendants.
|
|
302
118
|
|
|
303
119
|
```ts
|
|
304
|
-
|
|
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.
|
|
120
|
+
import { honey } from "@lovrozagar/honey"
|
|
317
121
|
|
|
318
|
-
|
|
122
|
+
type Env = { GREETING: string }
|
|
319
123
|
|
|
320
|
-
|
|
124
|
+
const app = honey<Env>().basePath("/api").context({ startedAt: Date.now() })
|
|
321
125
|
|
|
322
|
-
|
|
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
|
-
})
|
|
126
|
+
app.get("/hello").handler((ctx) => ctx.res.json("ok", { greeting: ctx.env.GREETING, since: ctx.startedAt }))
|
|
328
127
|
|
|
329
|
-
|
|
330
|
-
// GET /files/a/b/c → ctx.params.path === "a/b/c"
|
|
331
|
-
return ctx.res.json("ok", { path: ctx.params.path })
|
|
332
|
-
})
|
|
128
|
+
export default app
|
|
333
129
|
```
|
|
334
130
|
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
```
|
|
131
|
+
`context()` values are fixed at registration; per-request data belongs in middleware, which passes it through
|
|
132
|
+
`next({ … })`.
|
|
363
133
|
|
|
364
|
-
|
|
134
|
+
Settings sit on the same chain and apply to the whole app:
|
|
365
135
|
|
|
366
136
|
```ts
|
|
367
|
-
|
|
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
|
-
```
|
|
137
|
+
import { honey } from "@lovrozagar/honey"
|
|
373
138
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
|
386
|
-
|
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
| `
|
|
401
|
-
| `
|
|
402
|
-
| `
|
|
403
|
-
| `ctx
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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:
|
|
139
|
+
const app = honey()
|
|
140
|
+
.trailingSlash("strip")
|
|
141
|
+
.encodedSlashes("reject")
|
|
142
|
+
.outputValidation("always")
|
|
143
|
+
.errorFormat("problem+json")
|
|
144
|
+
.onNotFound((ctx) => new Response(`no route for ${new URL(ctx.req.url).pathname}`, { status: 404 }))
|
|
145
|
+
.onMethodNotAllowed((ctx) => new Response(null, { headers: { allow: ctx.allowed.join(", ") }, status: 405 }))
|
|
146
|
+
|
|
147
|
+
app.get("/").handler((ctx) => ctx.res.text("ok", "up"))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Setting | What it does |
|
|
151
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------- |
|
|
152
|
+
| `basePath(prefix)` | rebases every route and OpenAPI path registered through the handle |
|
|
153
|
+
| `context(values)` | static values on `ctx`, typed into the handler |
|
|
154
|
+
| `use(mw)` / `use(path, mw)` | middleware for this handle and its descendants, or for one path |
|
|
155
|
+
| `route(prefix, sub)` | mounts a sub-app under a prefix |
|
|
156
|
+
| `meta(values)` | app-level route meta every route inherits |
|
|
157
|
+
| `errorFactory(factory)`, `defaultErrors(…)` | the error factory routes throw from, and the keys every route declares |
|
|
158
|
+
| `defaultBoundary(key)` | the error an unexpected throw becomes |
|
|
159
|
+
| `onError(handler)` | maps a thrown value to a response before error telemetry |
|
|
160
|
+
| `onNotFound(handler)`, `onMethodNotAllowed(…)` | the two misses |
|
|
161
|
+
| `logger(logger \| false)`, `requestLog(…)` | where lines go, and the one-line-per-request log |
|
|
162
|
+
| `telemetry(value)` | the tracing adapter (`otelAdapter`) |
|
|
163
|
+
| `errorFormat(format)` | `"honey"` (the default) or `"problem+json"` (RFC 9457) |
|
|
164
|
+
| `defaultErrorFormatter`, `customErrorFormatter` | your own error body, schema-checked |
|
|
165
|
+
| `outputValidation(mode)` | what a response that does not match its `.output()` does |
|
|
166
|
+
| `trailingSlash`, `encodedSlashes`, `stripPrefix` | path normalization, and the prefix a gateway already consumed |
|
|
167
|
+
| `wsAdapter(adapter)` | the WebSocket adapter, when `serve()` does not pick one |
|
|
168
|
+
| `fetch(request, env, ctx)` | the Fetch handler an adapter calls |
|
|
169
|
+
|
|
170
|
+
Full reference: [the app](https://github.com/lovrozagar/honey/blob/main/docs/app-settings.md).
|
|
171
|
+
|
|
172
|
+
## Routing
|
|
173
|
+
|
|
174
|
+
One method per call (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`), `all` for any, and `on` for a named
|
|
175
|
+
set. Paths take `:params` and a trailing `*` wildcard; `meta()` carries what the document says about the route.
|
|
416
176
|
|
|
417
177
|
```ts
|
|
418
|
-
import "@lovrozagar/honey
|
|
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
|
-
```
|
|
178
|
+
import { honey } from "@lovrozagar/honey"
|
|
179
|
+
import { serveOpenApi } from "@lovrozagar/honey/openapi"
|
|
424
180
|
|
|
425
|
-
|
|
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.
|
|
181
|
+
const app = honey()
|
|
428
182
|
|
|
429
|
-
```ts
|
|
430
183
|
app
|
|
431
|
-
.
|
|
432
|
-
.
|
|
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
|
-
}
|
|
184
|
+
.get("/users/:id")
|
|
185
|
+
.meta({ operationId: "users.get", summary: "One user", tags: ["users"] })
|
|
186
|
+
.handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
|
|
450
187
|
|
|
451
|
-
|
|
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
|
-
```
|
|
188
|
+
app.delete("/users/:id").handler((ctx) => ctx.res.noContent())
|
|
462
189
|
|
|
463
|
-
|
|
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
|
-
},
|
|
472
|
-
)
|
|
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"))
|
|
190
|
+
serveOpenApi(app, { title: "Users", version: "1.0.0", docs: "scalar" })
|
|
482
191
|
```
|
|
483
192
|
|
|
484
|
-
|
|
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).
|
|
193
|
+
Sub-apps mount with `route()`, and `on()` registers one handler for several methods:
|
|
529
194
|
|
|
530
195
|
```ts
|
|
531
|
-
|
|
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
|
-
```
|
|
196
|
+
import { honey } from "@lovrozagar/honey"
|
|
549
197
|
|
|
550
|
-
|
|
198
|
+
const admin = honey()
|
|
199
|
+
admin.get("/stats").handler((ctx) => ctx.res.json("ok", { users: 1 }))
|
|
551
200
|
|
|
552
|
-
|
|
553
|
-
app
|
|
554
|
-
|
|
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 }))
|
|
201
|
+
const app = honey()
|
|
202
|
+
app.route("/admin", admin)
|
|
203
|
+
app.on(["POST", "PUT"], "/webhooks/:source").handler((ctx) => ctx.res.json("accepted", { from: ctx.params.source }))
|
|
557
204
|
```
|
|
558
205
|
|
|
559
|
-
|
|
206
|
+
A gateway merges the route trees its services publish, each tree tagged with meta of its own:
|
|
560
207
|
|
|
561
208
|
```ts
|
|
562
|
-
import {
|
|
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):
|
|
209
|
+
import { mergeTree } from "@lovrozagar/honey"
|
|
210
|
+
import type { RouteTree } from "@lovrozagar/honey/tree"
|
|
584
211
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
- With schemas that expose no JSON Schema (Valibot, Yup, Effect), one value is a scalar and a repeated key is an array.
|
|
212
|
+
declare const orders: RouteTree
|
|
213
|
+
declare const billing: RouteTree
|
|
588
214
|
|
|
589
|
-
|
|
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 }
|
|
215
|
+
const gateway = mergeTree([orders, { service: "orders" }], [billing, { service: "billing" }])
|
|
595
216
|
```
|
|
596
217
|
|
|
597
|
-
|
|
218
|
+
Full reference: [routing](https://github.com/lovrozagar/honey/blob/main/docs/routing.md).
|
|
598
219
|
|
|
599
|
-
|
|
220
|
+
## Input and output
|
|
600
221
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
.post("/v")
|
|
605
|
-
.input({ json: v.object({ n: v.number() }) })
|
|
606
|
-
.handler((ctx) => ctx.res.json("ok", ctx.input.json))
|
|
607
|
-
```
|
|
222
|
+
`.input()` validates the request: `params`, `search`, `headers`, `json`, `form` and `text`, each a Standard Schema.
|
|
223
|
+
`.output()` declares what the route answers, by media type and status key, and the declared response headers. A
|
|
224
|
+
media type other than JSON is just another key, so an image or a CSV is as typed as a JSON body:
|
|
608
225
|
|
|
609
|
-
|
|
226
|
+
```ts
|
|
227
|
+
import { honey } from "@lovrozagar/honey"
|
|
228
|
+
import * as z from "zod"
|
|
610
229
|
|
|
611
|
-
|
|
230
|
+
const app = honey()
|
|
612
231
|
|
|
613
|
-
```ts
|
|
614
232
|
app
|
|
615
|
-
.
|
|
233
|
+
.get("/avatar/:id")
|
|
616
234
|
.output({
|
|
617
|
-
"
|
|
618
|
-
|
|
619
|
-
ok: z.object({ id: z.string() }),
|
|
620
|
-
},
|
|
235
|
+
"image/png": { ok: z.instanceof(Uint8Array) },
|
|
236
|
+
headers: { ok: z.object({ etag: z.string() }) },
|
|
621
237
|
})
|
|
622
|
-
.handler((ctx) =>
|
|
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")
|
|
238
|
+
.handler((ctx) =>
|
|
239
|
+
ctx.res.binary("ok", new Uint8Array([137, 80, 78, 71]), { contentType: "image/png", headers: { etag: '"v1"' } }),
|
|
240
|
+
)
|
|
651
241
|
```
|
|
652
242
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
<!-- snippet:name errors-app -->
|
|
243
|
+
A body you do not want in memory is input too: `readableStream()` hands the handler the stream itself.
|
|
656
244
|
|
|
657
245
|
```ts
|
|
658
|
-
import {
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
})
|
|
246
|
+
import { honey } from "@lovrozagar/honey"
|
|
247
|
+
import { readableStream } from "@lovrozagar/honey/input"
|
|
248
|
+
import * as z from "zod"
|
|
670
249
|
|
|
671
250
|
const app = honey()
|
|
672
|
-
.errorFactory(errors)
|
|
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
251
|
|
|
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
252
|
app
|
|
724
|
-
.post("/
|
|
725
|
-
.
|
|
726
|
-
.
|
|
727
|
-
|
|
728
|
-
|
|
253
|
+
.post("/ingest")
|
|
254
|
+
.input({ json: readableStream(z.object({ chunks: z.number(), fileName: z.string() })) })
|
|
255
|
+
.handler(async (ctx) => {
|
|
256
|
+
/* the schema is what the document and the clients promise; the body itself stays on the request */
|
|
257
|
+
const reader = (ctx.req.body as ReadableStream<Uint8Array>).getReader()
|
|
258
|
+
await reader.cancel()
|
|
259
|
+
return ctx.res.json("accepted", { taken: true })
|
|
729
260
|
})
|
|
730
261
|
```
|
|
731
262
|
|
|
732
|
-
|
|
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
|
|
263
|
+
Content negotiation, when a route answers in more than one shape, reads the request directly:
|
|
777
264
|
|
|
778
265
|
```ts
|
|
779
|
-
import {
|
|
266
|
+
import { accepts } from "@lovrozagar/honey/accepts"
|
|
267
|
+
import { honey } from "@lovrozagar/honey"
|
|
780
268
|
|
|
781
|
-
const
|
|
782
|
-
const app = honey().errorFactory(errors)
|
|
269
|
+
const app = honey()
|
|
783
270
|
|
|
784
|
-
|
|
785
|
-
const
|
|
786
|
-
if (
|
|
787
|
-
return
|
|
271
|
+
app.get("/report").handler((ctx) => {
|
|
272
|
+
const type = accepts(ctx.req, ["text/csv", "application/json"])
|
|
273
|
+
if (type === "text/csv") return ctx.res.csv("ok", "name,count\nada,1\n")
|
|
274
|
+
return ctx.res.json("ok", [{ count: 1, name: "ada" }])
|
|
788
275
|
})
|
|
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
276
|
```
|
|
853
277
|
|
|
854
|
-
|
|
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"
|
|
866
|
-
|
|
867
|
-
const api = app.use(
|
|
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
|
-
)
|
|
874
|
-
```
|
|
278
|
+
Full reference: [input and output](https://github.com/lovrozagar/honey/blob/main/docs/input-output.md).
|
|
875
279
|
|
|
876
|
-
|
|
280
|
+
## Responses
|
|
877
281
|
|
|
878
|
-
|
|
282
|
+
`ctx.res` builds a typed response: `json`, `text`, `html`, `xml`, `csv`, `binary`, `noContent`, `redirect`, `raw` for
|
|
283
|
+
a `Response` you made yourself, and `sse`, `stream` and `generate` for bodies that arrive over time. The first
|
|
284
|
+
argument is the status key the route declared, which is what makes the payload's type the right one.
|
|
879
285
|
|
|
880
286
|
```ts
|
|
881
|
-
import {
|
|
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
|
-
)
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
`createLogger` writes one JSON line per call (`level`, `msg`, `time`, plus `base`). `ctx.log.info("hello")` / `ctx.log.info({ k: 1 }, "hello")`. The logged `path` is the normalized `ctx.path` (no query string). A throwing `log`, `skip` or sink is reported with `console.error` and never turns the response into a 500.
|
|
287
|
+
import { honey } from "@lovrozagar/honey"
|
|
903
288
|
|
|
904
|
-
|
|
289
|
+
const app = honey()
|
|
905
290
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
291
|
+
app.get("/page").handler((ctx) => ctx.res.html("ok", "<h1>hi</h1>"))
|
|
292
|
+
app.get("/feed").handler((ctx) => ctx.res.xml("ok", "<feed />"))
|
|
293
|
+
app.get("/old").handler((ctx) => ctx.res.redirect("/new"))
|
|
294
|
+
app.get("/nothing").handler((ctx) => ctx.res.noContent())
|
|
295
|
+
app.get("/bytes").handler((ctx) =>
|
|
296
|
+
ctx.res.stream(async (writable) => {
|
|
297
|
+
const writer = writable.getWriter()
|
|
298
|
+
await writer.write(new TextEncoder().encode("chunk"))
|
|
299
|
+
await writer.close()
|
|
915
300
|
}),
|
|
916
301
|
)
|
|
917
302
|
```
|
|
918
303
|
|
|
919
|
-
|
|
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`
|
|
304
|
+
Caching, entity tags and byte ranges are middleware and helpers over any of those:
|
|
933
305
|
|
|
934
306
|
```ts
|
|
307
|
+
import { honey } from "@lovrozagar/honey"
|
|
308
|
+
import { cacheControl } from "@lovrozagar/honey/cache-control"
|
|
935
309
|
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
310
|
import { prettyJson } from "@lovrozagar/honey/pretty-json"
|
|
311
|
+
import { rangeResponse } from "@lovrozagar/honey/range"
|
|
1025
312
|
|
|
1026
|
-
const
|
|
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
|
|
313
|
+
const video = new Blob([new Uint8Array(1024)], { type: "video/mp4" })
|
|
1033
314
|
|
|
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
315
|
const app = honey()
|
|
1067
|
-
.
|
|
1068
|
-
.
|
|
1069
|
-
|
|
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.
|
|
316
|
+
.use(etag())
|
|
317
|
+
.use(prettyJson({ query: "pretty" }))
|
|
318
|
+
.use(cacheControl({ public: true, maxAge: 60, staleWhileRevalidate: 300 }))
|
|
1086
319
|
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
})
|
|
320
|
+
app.get("/prices").handler((ctx) => ctx.res.json("ok", { eur: 1.08 }))
|
|
321
|
+
app.get("/video").handler((ctx) => ctx.res.raw(rangeResponse(ctx.req, video)))
|
|
1101
322
|
```
|
|
1102
323
|
|
|
1103
|
-
`
|
|
324
|
+
`etag()` answers a conditional GET with 304, `rangeResponse` a `Range` request with 206, and `cacheControl` sets the
|
|
325
|
+
header once for every route under it. `httpDate` from the same subpath formats an HTTP date, and `notModified` from
|
|
326
|
+
`@lovrozagar/honey/etag` turns a response into its 304.
|
|
1104
327
|
|
|
1105
|
-
|
|
328
|
+
Streaming bodies clean up when the client leaves — the generator's `finally` runs:
|
|
1106
329
|
|
|
1107
|
-
|
|
330
|
+
```ts run
|
|
331
|
+
import { honey } from "@lovrozagar/honey"
|
|
332
|
+
import { serve } from "@lovrozagar/honey/serve"
|
|
1108
333
|
|
|
1109
|
-
|
|
1110
|
-
|
|
334
|
+
let ended = (_: string) => {}
|
|
335
|
+
const cleanedUp = new Promise<string>((resolve) => (ended = resolve))
|
|
336
|
+
|
|
337
|
+
const app = honey()
|
|
1111
338
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
339
|
+
app.get("/ticks").handler((ctx) =>
|
|
340
|
+
ctx.res.sse(
|
|
341
|
+
async (stream) => {
|
|
342
|
+
try {
|
|
343
|
+
for (let tick = 1; !stream.closed; tick++) {
|
|
344
|
+
await stream.send({ data: String(tick), event: "tick", id: String(tick) })
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
346
|
+
}
|
|
347
|
+
} finally {
|
|
348
|
+
stream.close()
|
|
349
|
+
ended("cleaned up")
|
|
350
|
+
}
|
|
1119
351
|
},
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
rewritePath: (filePath) => (filePath === "/" ? "/index.html" : filePath),
|
|
1123
|
-
}),
|
|
352
|
+
{ defaultRetry: 3000 },
|
|
353
|
+
),
|
|
1124
354
|
)
|
|
1125
|
-
```
|
|
1126
355
|
|
|
1127
|
-
|
|
356
|
+
const server = await serve(app, { hostname: "127.0.0.1", port: 0 })
|
|
357
|
+
const leave = new AbortController()
|
|
358
|
+
const response = await fetch(`${server.url}/ticks`, { signal: leave.signal })
|
|
359
|
+
const reader = (response.body as ReadableStream<Uint8Array>).getReader()
|
|
360
|
+
const first = new TextDecoder().decode((await reader.read()).value)
|
|
361
|
+
console.log(first.includes("retry: 3000") ? "retry sent" : "no retry")
|
|
362
|
+
leave.abort()
|
|
363
|
+
console.log(await cleanedUp)
|
|
364
|
+
await server.close()
|
|
365
|
+
// prints: retry sent
|
|
366
|
+
// prints: cleaned up
|
|
367
|
+
```
|
|
1128
368
|
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
369
|
+
Full reference: [streaming](https://github.com/lovrozagar/honey/blob/main/docs/streaming.md),
|
|
370
|
+
[media types](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/media-types.md),
|
|
371
|
+
[range requests](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/range.md) and
|
|
372
|
+
[Cache-Control](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/cache-control.md).
|
|
1132
373
|
|
|
1133
|
-
|
|
374
|
+
## Errors
|
|
1134
375
|
|
|
1135
|
-
|
|
376
|
+
`defineErrors` maps your own keys to HTTP statuses and gives you the factory a route throws from. A route declares
|
|
377
|
+
which keys it can throw with `.errors()`, and those keys are what the document and every generated client know
|
|
378
|
+
about.
|
|
1136
379
|
|
|
1137
380
|
```ts
|
|
1138
|
-
|
|
381
|
+
import { defineErrors, honey } from "@lovrozagar/honey"
|
|
382
|
+
|
|
383
|
+
const errors = defineErrors({ out_of_stock: "conflict" })
|
|
384
|
+
|
|
385
|
+
const app = honey().errorFormat("problem+json")
|
|
386
|
+
|
|
387
|
+
app
|
|
388
|
+
.post("/orders")
|
|
389
|
+
.errors(errors, "out_of_stock")
|
|
390
|
+
.handler(() => {
|
|
391
|
+
throw errors.out_of_stock()
|
|
392
|
+
})
|
|
1139
393
|
```
|
|
1140
394
|
|
|
1141
|
-
|
|
395
|
+
`errorFormat("problem+json")` answers RFC 9457 Problem Details instead of honey's own body. Anything thrown becomes a
|
|
396
|
+
`HoneyError`; `onError` sees it first, and `ERROR_META` is the symbol a thrown value carries its status under, so a
|
|
397
|
+
value from elsewhere can present itself as one. `createErrorResponse` builds the body outside a request, which is
|
|
398
|
+
what a custom formatter or a test harness needs.
|
|
1142
399
|
|
|
1143
400
|
```ts
|
|
1144
|
-
import {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
401
|
+
import {
|
|
402
|
+
createErrorResponse,
|
|
403
|
+
defineErrors,
|
|
404
|
+
ERROR_META,
|
|
405
|
+
type ErrorFormatter,
|
|
406
|
+
HoneyError,
|
|
407
|
+
honey,
|
|
408
|
+
} from "@lovrozagar/honey"
|
|
409
|
+
|
|
410
|
+
const errors = defineErrors({ db_timeout: "service_unavailable" })
|
|
411
|
+
|
|
412
|
+
const app = honey().onError((error, ctx) => {
|
|
413
|
+
if (error instanceof HoneyError && error.errorKey === "db_timeout") return ctx.jsonFromError(error)
|
|
414
|
+
return undefined
|
|
1157
415
|
})
|
|
1158
|
-
```
|
|
1159
416
|
|
|
1160
|
-
|
|
417
|
+
app.get("/").handler((ctx) => ctx.res.text("ok", "up"))
|
|
1161
418
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
```ts
|
|
1165
|
-
import { routeTree } from "./_gen/routes.gen.ts"
|
|
419
|
+
/* a thrown value of your own, carrying the status honey should answer with */
|
|
420
|
+
const refusal = Object.assign(new Error("nope"), { [ERROR_META]: { errorKey: "forbidden", status: 403 } })
|
|
1166
421
|
|
|
1167
|
-
|
|
1168
|
-
|
|
422
|
+
/* the body an error answers with: the default shape, plus whatever your API adds to it */
|
|
423
|
+
const formatter: ErrorFormatter = (error, defaultShape) => ({ ...defaultShape, retriable: error.status >= 500 })
|
|
424
|
+
const answer = createErrorResponse(errors.db_timeout(), formatter, null, { format: "problem+json" })
|
|
1169
425
|
```
|
|
1170
426
|
|
|
1171
|
-
|
|
1172
|
-
|
|
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.
|
|
427
|
+
Full reference: [problem+json](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/problem-json.md) and
|
|
428
|
+
[logging, errors and tracing](https://github.com/lovrozagar/honey/blob/main/docs/observability.md).
|
|
1175
429
|
|
|
1176
|
-
|
|
430
|
+
## Middleware
|
|
1177
431
|
|
|
1178
|
-
|
|
432
|
+
A middleware is a function of `(ctx, next)`. Calling `next({ … })` adds typed values to the context every route below
|
|
433
|
+
it sees — that is how per-request data (a user, a tenant, a database handle) reaches a handler without a global.
|
|
1179
434
|
|
|
1180
435
|
```ts
|
|
1181
|
-
import {
|
|
436
|
+
import { type MiddlewareFn, honey } from "@lovrozagar/honey"
|
|
437
|
+
import { cors } from "@lovrozagar/honey/cors"
|
|
438
|
+
import { rateLimit } from "@lovrozagar/honey/rate-limit"
|
|
439
|
+
import { secureHeaders } from "@lovrozagar/honey/secure-headers"
|
|
440
|
+
|
|
441
|
+
type UserCtx = { user: { id: string } | null }
|
|
1182
442
|
|
|
1183
|
-
|
|
443
|
+
const withUser: MiddlewareFn<{ req: Request }, UserCtx> = (ctx, next) => {
|
|
444
|
+
const token = ctx.req.headers.get("authorization")
|
|
445
|
+
return next({ user: token === null ? null : { id: "u1" } })
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const app = honey()
|
|
449
|
+
.use(secureHeaders())
|
|
450
|
+
.use(cors({ origin: ["https://app.example.com"], credentials: true }))
|
|
451
|
+
.use(rateLimit({ limit: 100, windowMs: 60_000 }))
|
|
452
|
+
.use(withUser)
|
|
1184
453
|
|
|
1185
|
-
|
|
1186
|
-
gateway.all("/*").proxy({ destination: (ctx) => `https://${ctx.meta.worker}.internal${ctx.path}` })
|
|
454
|
+
app.get("/me").handler((ctx) => ctx.res.json("ok", { user: ctx.user }))
|
|
1187
455
|
```
|
|
1188
456
|
|
|
1189
|
-
|
|
457
|
+
`createMiddleware` is the same thing with the context type given once, and `defineMiddleware` keeps a middleware
|
|
458
|
+
reusable across apps:
|
|
1190
459
|
|
|
1191
460
|
```ts
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
461
|
+
import { createMiddleware, defineMiddleware, honey } from "@lovrozagar/honey"
|
|
462
|
+
|
|
463
|
+
const requireKey = createMiddleware<{ req: Request }>((ctx, next) =>
|
|
464
|
+
ctx.req.headers.get("x-api-key") === null ? new Response(null, { status: 401 }) : next(),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
const stamp = defineMiddleware({ fn: (_ctx: { req: Request }, next) => next({ at: Date.now() }) })
|
|
1195
468
|
|
|
1196
|
-
|
|
469
|
+
const app = honey().use(requireKey).use(stamp)
|
|
470
|
+
app.get("/me").handler((ctx) => ctx.res.json("ok", { at: ctx.at }))
|
|
1197
471
|
```
|
|
1198
472
|
|
|
1199
|
-
|
|
473
|
+
The bundled set, each its own subpath so an app that does not import it bundles none of it:
|
|
1200
474
|
|
|
1201
|
-
|
|
475
|
+
```ts
|
|
476
|
+
import { honey } from "@lovrozagar/honey"
|
|
477
|
+
import { bodyLimit } from "@lovrozagar/honey/body-limit"
|
|
478
|
+
import { compress } from "@lovrozagar/honey/compress"
|
|
479
|
+
import { csrf } from "@lovrozagar/honey/csrf"
|
|
480
|
+
import { ipRestrict } from "@lovrozagar/honey/ip-restrict"
|
|
481
|
+
import { poweredBy } from "@lovrozagar/honey/powered-by"
|
|
482
|
+
import { requestId } from "@lovrozagar/honey/request-id"
|
|
483
|
+
import { serverTiming } from "@lovrozagar/honey/server-timing"
|
|
484
|
+
import { timeout } from "@lovrozagar/honey/timeout"
|
|
1202
485
|
|
|
1203
|
-
|
|
486
|
+
const app = honey()
|
|
487
|
+
.use(requestId())
|
|
488
|
+
.use(poweredBy())
|
|
489
|
+
.use(serverTiming())
|
|
490
|
+
.use(timeout({ duration: 10_000 }))
|
|
491
|
+
.use(bodyLimit({ maxSize: 1024 * 1024 }))
|
|
492
|
+
.use(compress({ threshold: 2048 }))
|
|
493
|
+
.use(csrf({ origin: ["https://app.example.com"] }))
|
|
494
|
+
.use(ipRestrict({ allowList: ["203.0.113.0/24"] }))
|
|
495
|
+
|
|
496
|
+
app.get("/report").handler((ctx) => ctx.res.text("ok", "row\n".repeat(5000)))
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
| Middleware | From | What it does |
|
|
500
|
+
| ------------------------- | ---------------------------------- | ---------------------------------------------------------- |
|
|
501
|
+
| `cors(options)` | `@lovrozagar/honey/cors` | the CORS headers and the preflight answer |
|
|
502
|
+
| `secureHeaders(options?)` | `@lovrozagar/honey/secure-headers` | CSP, HSTS and the rest of the hardening headers |
|
|
503
|
+
| `csrf(options?)` | `@lovrozagar/honey/csrf` | refuses a cross-origin unsafe method |
|
|
504
|
+
| `rateLimit(options)` | `@lovrozagar/honey/rate-limit` | a window per key, with `memoryStore()` or one of your own |
|
|
505
|
+
| `compress(options?)` | `@lovrozagar/honey/compress` | gzip or brotli over a threshold |
|
|
506
|
+
| `cacheControl(options)` | `@lovrozagar/honey/cache-control` | the `Cache-Control` header, and `httpDate` |
|
|
507
|
+
| `etag(options?)` | `@lovrozagar/honey/etag` | an entity tag, 304 on a conditional GET, and `notModified` |
|
|
508
|
+
| `prettyJson(options?)` | `@lovrozagar/honey/pretty-json` | indents a JSON body when the query asks |
|
|
509
|
+
| `requestId(options?)` | `@lovrozagar/honey/request-id` | one id per request, on the response and in the log |
|
|
510
|
+
| `timeout(options)` | `@lovrozagar/honey/timeout` | a deadline per request |
|
|
511
|
+
| `bodyLimit(options)` | `@lovrozagar/honey/body-limit` | refuses a body over a size |
|
|
512
|
+
| `ipRestrict(options)` | `@lovrozagar/honey/ip-restrict` | an allow or deny list over `ctx.ip` |
|
|
513
|
+
| `serverTiming()` | `@lovrozagar/honey/server-timing` | `Server-Timing`, and `ctx.timing` to add to it |
|
|
514
|
+
| `poweredBy(options?)` | `@lovrozagar/honey/powered-by` | the `X-Powered-By` header |
|
|
515
|
+
| `staticFiles(options)` | `@lovrozagar/honey/static` | serves files under a prefix |
|
|
516
|
+
| `curlLogger(options?)` | `@lovrozagar/honey/curl-logger` | logs each request as the command that would repeat it |
|
|
517
|
+
|
|
518
|
+
Full reference: [middleware](https://github.com/lovrozagar/honey/blob/main/docs/middleware.md),
|
|
519
|
+
[rate limiting](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/rate-limit.md) and
|
|
520
|
+
[compression](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/compress.md).
|
|
521
|
+
|
|
522
|
+
## The request context
|
|
523
|
+
|
|
524
|
+
The handler's `ctx` is one object, and these keys are reserved on it: `req`, `env`, `executionCtx`, `path`,
|
|
525
|
+
`params`, `search`, `searchAll`, `headers`, `input`, `ip`, `res`, `errors`, `error`, `meta`, `routePattern`,
|
|
526
|
+
`cookies`, `log`, `logger`, `signal`, `background` and `tap`. Everything else on `ctx` is yours, from `context()` or
|
|
527
|
+
from a middleware's `next({ … })`.
|
|
1204
528
|
|
|
1205
529
|
```ts
|
|
1206
|
-
|
|
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
|
-
})
|
|
530
|
+
import { honey } from "@lovrozagar/honey"
|
|
1213
531
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
532
|
+
const app = honey()
|
|
533
|
+
|
|
534
|
+
app.get("/where/:id").handler((ctx) => {
|
|
535
|
+
ctx.log.info({ id: ctx.params.id }, "asked")
|
|
536
|
+
ctx.background(Promise.resolve())
|
|
537
|
+
return ctx.res.json("ok", {
|
|
538
|
+
accept: ctx.headers.accept ?? null,
|
|
539
|
+
aborted: ctx.signal.aborted,
|
|
540
|
+
id: ctx.params.id,
|
|
541
|
+
ip: ctx.ip,
|
|
542
|
+
pattern: ctx.routePattern,
|
|
543
|
+
path: ctx.path,
|
|
544
|
+
tags: ctx.searchAll.tag ?? [],
|
|
545
|
+
verbose: ctx.search.verbose ?? null,
|
|
546
|
+
})
|
|
547
|
+
})
|
|
1219
548
|
```
|
|
1220
549
|
|
|
1221
|
-
|
|
550
|
+
`background(promise)` keeps work alive past the response where the runtime allows it, `signal` aborts when the client
|
|
551
|
+
leaves, and `log` is the request's logger with the request id already bound. `HoneyContext` is the class behind it
|
|
552
|
+
and `HoneyRes` the response builder, both exported for a helper of your own that takes either.
|
|
1222
553
|
|
|
1223
|
-
|
|
554
|
+
## Cookies
|
|
1224
555
|
|
|
1225
|
-
|
|
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. |
|
|
556
|
+
`ctx.cookies` reads them, and a response sets them. Signed cookies are a signer you build from your secrets, newest
|
|
557
|
+
first, so a rotation keeps the old ones verifying:
|
|
1232
558
|
|
|
1233
|
-
|
|
559
|
+
```ts
|
|
560
|
+
import { honey } from "@lovrozagar/honey"
|
|
561
|
+
import { createCookieSigner } from "@lovrozagar/honey/cookie-sign"
|
|
1234
562
|
|
|
1235
|
-
|
|
563
|
+
const signer = createCookieSigner([process.env.COOKIE_SECRET ?? ""])
|
|
564
|
+
|
|
565
|
+
const app = honey()
|
|
1236
566
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
567
|
+
app.post("/login").handler(async (ctx) => {
|
|
568
|
+
const session = await signer.sign("user-1")
|
|
569
|
+
const cookie = { value: session, httpOnly: true, secure: true, sameSite: "lax" as const, path: "/" }
|
|
570
|
+
return ctx.res.json("ok", { signedIn: true }, { cookies: { session: cookie } })
|
|
571
|
+
})
|
|
1242
572
|
|
|
1243
|
-
|
|
573
|
+
app.get("/me").handler(async (ctx) => {
|
|
574
|
+
const raw = ctx.cookies.session
|
|
575
|
+
const user = raw === undefined ? null : await signer.verify(raw)
|
|
576
|
+
return ctx.res.json("ok", { user })
|
|
577
|
+
})
|
|
578
|
+
```
|
|
1244
579
|
|
|
1245
|
-
|
|
580
|
+
`@lovrozagar/honey/cookie` has the header functions on their own (`serializeCookie`, `parseCookieHeader`) and
|
|
581
|
+
`@lovrozagar/honey/cookie-sign` the one-shot `sign` and `verify`:
|
|
1246
582
|
|
|
1247
583
|
```ts
|
|
1248
|
-
import {
|
|
1249
|
-
|
|
1250
|
-
await server.shutdown(10_000)
|
|
1251
|
-
```
|
|
584
|
+
import { parseCookieHeader, serializeCookie } from "@lovrozagar/honey/cookie"
|
|
585
|
+
import { sign, verify } from "@lovrozagar/honey/cookie-sign"
|
|
1252
586
|
|
|
1253
|
-
|
|
587
|
+
declare const req: Request
|
|
588
|
+
declare const SECRET: string
|
|
589
|
+
declare const OLD_SECRET: string
|
|
1254
590
|
|
|
1255
|
-
|
|
591
|
+
const jar = parseCookieHeader(req.headers.get("cookie") ?? "")
|
|
592
|
+
const header = serializeCookie("theme", { value: "dark", httpOnly: true, path: "/", sameSite: "lax" })
|
|
1256
593
|
|
|
1257
|
-
|
|
594
|
+
/* a signature bound to the cookie's name; verify takes every secret still accepted, newest first */
|
|
595
|
+
const signed = await sign("abc", SECRET, { name: "sid" })
|
|
596
|
+
const value = await verify(signed, [SECRET, OLD_SECRET], { name: "sid" })
|
|
597
|
+
```
|
|
1258
598
|
|
|
1259
|
-
|
|
599
|
+
Full reference: [cookies](https://github.com/lovrozagar/honey/blob/main/docs/cookies.md).
|
|
1260
600
|
|
|
1261
|
-
|
|
601
|
+
## Streaming, WebSockets and realtime
|
|
1262
602
|
|
|
1263
|
-
|
|
603
|
+
A WebSocket route is `ws(app, path)`; a resumable connection — one logical subscription over WebSocket, SSE or
|
|
604
|
+
long-poll, whichever the client can hold — is `realtime(app, path, options)`. Both publish to rooms, and a plain
|
|
605
|
+
route can publish into them.
|
|
1264
606
|
|
|
1265
607
|
```ts
|
|
1266
|
-
import {
|
|
1267
|
-
|
|
1268
|
-
|
|
608
|
+
import { honey } from "@lovrozagar/honey"
|
|
609
|
+
import { realtime } from "@lovrozagar/honey/realtime"
|
|
610
|
+
import { ws } from "@lovrozagar/honey/ws"
|
|
1269
611
|
|
|
1270
|
-
|
|
612
|
+
const app = honey()
|
|
1271
613
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
614
|
+
ws(app, "/echo").handler({
|
|
615
|
+
onMessage(_ctx, socket, data) {
|
|
616
|
+
socket.send(data)
|
|
617
|
+
},
|
|
618
|
+
})
|
|
1275
619
|
|
|
1276
|
-
app
|
|
620
|
+
const chat = realtime(app, "/chat/:room", {
|
|
621
|
+
namespace: "chat",
|
|
622
|
+
handler: (_ctx, conn) => {
|
|
623
|
+
conn.join("lobby")
|
|
624
|
+
conn.on("message", (payload) => conn.publish("lobby", payload))
|
|
625
|
+
},
|
|
626
|
+
})
|
|
1277
627
|
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
}
|
|
628
|
+
app.post("/announce").handler(async (ctx) => {
|
|
629
|
+
chat.publish("lobby", await ctx.req.json())
|
|
630
|
+
return ctx.res.json("ok", { sent: true })
|
|
631
|
+
})
|
|
1281
632
|
```
|
|
1282
633
|
|
|
1283
|
-
|
|
634
|
+
A publisher outside a connection works the same way, which is what a ticker is:
|
|
1284
635
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
636
|
+
```ts
|
|
637
|
+
import { honey } from "@lovrozagar/honey"
|
|
638
|
+
import { realtime } from "@lovrozagar/honey/realtime"
|
|
1288
639
|
|
|
1289
|
-
|
|
640
|
+
const app = honey()
|
|
1290
641
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
642
|
+
const prices = realtime(app, "/prices", {
|
|
643
|
+
namespace: "prices",
|
|
644
|
+
handler: (_ctx, conn) => {
|
|
645
|
+
conn.join("ticker")
|
|
646
|
+
},
|
|
647
|
+
})
|
|
1297
648
|
|
|
1298
|
-
|
|
649
|
+
setInterval(() => prices.publish("ticker", { eur: 1.08, at: Date.now() }), 1000)
|
|
650
|
+
```
|
|
1299
651
|
|
|
1300
|
-
`
|
|
652
|
+
`serve()` picks the adapter for Node and Bun; a Worker sets one itself. `createWsSession` from
|
|
653
|
+
`@lovrozagar/honey/ws/session` is the session runtime those adapters share, for an adapter of your own:
|
|
1301
654
|
|
|
1302
655
|
```ts
|
|
1303
|
-
import {
|
|
656
|
+
import { createWsSession } from "@lovrozagar/honey/ws/session"
|
|
657
|
+
import { bunWebSocket } from "@lovrozagar/honey/ws/bun"
|
|
658
|
+
import { cfWebSocket } from "@lovrozagar/honey/ws/cloudflare"
|
|
659
|
+
import { nodeWebSocket } from "@lovrozagar/honey/ws/node"
|
|
660
|
+
import { honey } from "@lovrozagar/honey"
|
|
1304
661
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
662
|
+
/* each runtime's adapter; serve() sets the first two, a Worker entry the third */
|
|
663
|
+
const adapters = [bunWebSocket(), nodeWebSocket(), cfWebSocket()]
|
|
664
|
+
const app = honey().wsAdapter(cfWebSocket())
|
|
665
|
+
|
|
666
|
+
/* the ordered lane a custom adapter drives: open, then messages, then close */
|
|
667
|
+
const session = createWsSession({
|
|
668
|
+
callbacks: { onMessage: (_ctx, socket, data) => socket.send(data) },
|
|
669
|
+
ctx: {},
|
|
670
|
+
log: null,
|
|
671
|
+
reconnectToken: null,
|
|
672
|
+
route: "/echo",
|
|
673
|
+
})
|
|
1309
674
|
```
|
|
1310
675
|
|
|
1311
|
-
|
|
676
|
+
`nodeWebSocket()` needs the optional `ws` peer installed; the Bun and Workers adapters use their runtime's own
|
|
677
|
+
sockets. A resumed connection replays from the client's `Last-Event-ID` or `reconnect_token`.
|
|
1312
678
|
|
|
1313
|
-
|
|
679
|
+
Full reference: [WebSockets, realtime and SSE](https://github.com/lovrozagar/honey/blob/main/docs/realtime.md) and
|
|
680
|
+
[realtime resume](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/realtime-resume.md).
|
|
1314
681
|
|
|
1315
|
-
|
|
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
|
-
})
|
|
682
|
+
## Serving on Node, Bun and Cloudflare Workers
|
|
1329
683
|
|
|
1330
|
-
app.
|
|
1331
|
-
app.manifest({ path: "/manifest.json" })
|
|
1332
|
-
```
|
|
684
|
+
One app runs on each of them; only how it is served differs.
|
|
1333
685
|
|
|
1334
|
-
|
|
686
|
+
| Runtime | How it runs |
|
|
687
|
+
| ------------------ | --------------------------------------------------------------------------------- |
|
|
688
|
+
| Node | `serve(app)` from `@lovrozagar/honey/serve` |
|
|
689
|
+
| Bun | `serve(app)`, or `Bun.serve({ fetch: app.fetch })` |
|
|
690
|
+
| Cloudflare Workers | `export default app` — the Worker's `fetch` handler, with named exports beside it |
|
|
1335
691
|
|
|
1336
|
-
|
|
692
|
+
```ts
|
|
693
|
+
import { detectRuntime, honey, toFetchRequest } from "@lovrozagar/honey"
|
|
694
|
+
import { serve } from "@lovrozagar/honey/serve"
|
|
1337
695
|
|
|
1338
|
-
|
|
696
|
+
const app = honey()
|
|
697
|
+
app.get("/health").handler((ctx) => ctx.res.text("ok", detectRuntime()))
|
|
1339
698
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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 |
|
|
699
|
+
/* a request from an adapter that handed you its own Request subclass */
|
|
700
|
+
declare const incoming: Request
|
|
701
|
+
const request = toFetchRequest(incoming)
|
|
1346
702
|
|
|
1347
|
-
|
|
703
|
+
const server = await serve(app, { hostname: "127.0.0.1", maxRequestBodySize: 8 * 1024 * 1024, port: 0 })
|
|
704
|
+
console.log(server.url)
|
|
705
|
+
await server.close()
|
|
706
|
+
```
|
|
1348
707
|
|
|
1349
|
-
|
|
708
|
+
`@lovrozagar/honey/serve` is the one to reach for: it resolves the runtime itself and returns a `ServeHandle` with
|
|
709
|
+
the bound `url` and a `close()` that drains. `@lovrozagar/honey/node` is `node:http` directly, for an existing
|
|
710
|
+
server or a framework that hands you one:
|
|
1350
711
|
|
|
1351
|
-
|
|
712
|
+
```ts
|
|
713
|
+
import { honey } from "@lovrozagar/honey"
|
|
714
|
+
import { serve } from "@lovrozagar/honey/node"
|
|
1352
715
|
|
|
1353
|
-
|
|
716
|
+
const app = honey()
|
|
717
|
+
app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
|
|
1354
718
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
stripSecuritySchemes: ["legacy"],
|
|
1358
|
-
stripSecurityRequirements: ["legacy"],
|
|
1359
|
-
stripXExtensions: true, // or ["x-internal"]
|
|
1360
|
-
}
|
|
719
|
+
const server = await serve(app, { env: {}, port: 0 })
|
|
720
|
+
await server.close()
|
|
1361
721
|
```
|
|
1362
722
|
|
|
1363
|
-
|
|
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.
|
|
723
|
+
On Workers the app is the default export, and `app.fetch` the handler to wrap when you need to:
|
|
1366
724
|
|
|
1367
725
|
```ts
|
|
1368
|
-
|
|
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
|
-
```
|
|
726
|
+
import { honey } from "@lovrozagar/honey"
|
|
1396
727
|
|
|
1397
|
-
|
|
728
|
+
type Env = { readonly SECRET: string }
|
|
1398
729
|
|
|
1399
|
-
|
|
730
|
+
const app = honey<Env>()
|
|
731
|
+
app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
|
|
732
|
+
|
|
733
|
+
export default { fetch: (...args: Parameters<typeof app.fetch>) => app.fetch(...args) }
|
|
734
|
+
```
|
|
1400
735
|
|
|
1401
|
-
|
|
736
|
+
## Static files and proxying
|
|
1402
737
|
|
|
1403
|
-
|
|
738
|
+
`staticFiles()` serves what a resolver of yours returns, so the bytes can come from a bundle, a KV store or a disk
|
|
739
|
+
read — honey never assumes a filesystem.
|
|
1404
740
|
|
|
1405
741
|
```ts
|
|
1406
|
-
|
|
742
|
+
import { honey } from "@lovrozagar/honey"
|
|
743
|
+
import { cacheControl } from "@lovrozagar/honey/cache-control"
|
|
744
|
+
import { staticFiles } from "@lovrozagar/honey/static"
|
|
1407
745
|
|
|
1408
|
-
const
|
|
746
|
+
const assets = new Map<string, Blob>([["/app.js", new Blob(["console.log(1)"])]])
|
|
747
|
+
|
|
748
|
+
const app = honey()
|
|
749
|
+
.use(cacheControl({ public: true, maxAge: 3600 }))
|
|
750
|
+
.use(staticFiles({ prefix: "/assets", resolve: (_ctx, path) => assets.get(path) ?? null }))
|
|
1409
751
|
```
|
|
1410
752
|
|
|
1411
|
-
|
|
753
|
+
`proxy()` is a handler that forwards, which is what makes an app a gateway: you own the destination call, so the
|
|
754
|
+
upstream is explicit and nothing is reachable by accident.
|
|
1412
755
|
|
|
1413
|
-
|
|
756
|
+
```ts
|
|
757
|
+
import { honey } from "@lovrozagar/honey"
|
|
758
|
+
import { proxy } from "@lovrozagar/honey/proxy"
|
|
1414
759
|
|
|
1415
|
-
|
|
760
|
+
const app = honey()
|
|
1416
761
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
},
|
|
1423
|
-
|
|
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
|
-
})
|
|
762
|
+
app.all("/billing/*").handler(
|
|
763
|
+
proxy({
|
|
764
|
+
destination: (_ctx, url, init) => fetch(`https://billing.internal.example.com${url}`, init),
|
|
765
|
+
requestHeaders: { "x-gateway": "edge" },
|
|
766
|
+
timeout: 10_000,
|
|
767
|
+
}),
|
|
768
|
+
)
|
|
1432
769
|
```
|
|
1433
770
|
|
|
1434
|
-
|
|
771
|
+
Full reference: [static files](https://github.com/lovrozagar/honey/blob/main/docs/static.md) and
|
|
772
|
+
[proxying and gateways](https://github.com/lovrozagar/honey/blob/main/docs/proxy.md).
|
|
1435
773
|
|
|
1436
|
-
|
|
774
|
+
## The OpenAPI document
|
|
1437
775
|
|
|
1438
|
-
|
|
776
|
+
`serveOpenApi(app, options)` returns the app with the document and its reference UI mounted; `generateOpenApi` gives
|
|
777
|
+
you the document as a value instead, for a file you commit or a build step. Tags, servers with variables,
|
|
778
|
+
externalDocs and webhooks are all declared, never guessed.
|
|
1439
779
|
|
|
1440
780
|
```ts
|
|
1441
|
-
|
|
1442
|
-
|
|
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`).
|
|
781
|
+
import { honey } from "@lovrozagar/honey"
|
|
782
|
+
import { serveOpenApi } from "@lovrozagar/honey/openapi"
|
|
1454
783
|
|
|
1455
|
-
|
|
784
|
+
const app = honey()
|
|
1456
785
|
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
786
|
+
app
|
|
787
|
+
.get("/orders")
|
|
788
|
+
.meta({ summary: "List orders", tags: ["orders"] })
|
|
789
|
+
.handler((ctx) => ctx.res.json("ok", []))
|
|
790
|
+
|
|
791
|
+
serveOpenApi(app, {
|
|
792
|
+
title: "Shop",
|
|
793
|
+
version: "1.2.0",
|
|
794
|
+
servers: [{ url: "https://{region}.api.example.com", variables: { region: { default: "eu" } } }],
|
|
795
|
+
tags: [{ name: "orders", description: "Placing and tracking orders" }],
|
|
796
|
+
docs: "scalar",
|
|
797
|
+
})
|
|
798
|
+
```
|
|
1464
799
|
|
|
1465
|
-
|
|
800
|
+
The document as a value, the route manifest, YAML, and either reference page explicitly:
|
|
1466
801
|
|
|
1467
802
|
```ts
|
|
1468
|
-
|
|
1469
|
-
|
|
803
|
+
import { honey } from "@lovrozagar/honey"
|
|
804
|
+
import { generateManifest, generateOpenApi, serveManifest, toYaml } from "@lovrozagar/honey/openapi"
|
|
805
|
+
import { scalar } from "@lovrozagar/honey/openapi/scalar"
|
|
806
|
+
import { swagger } from "@lovrozagar/honey/openapi/swagger"
|
|
1470
807
|
|
|
1471
|
-
|
|
808
|
+
const app = honey()
|
|
809
|
+
app.get("/orders").handler((ctx) => ctx.res.json("ok", []))
|
|
1472
810
|
|
|
1473
|
-
|
|
811
|
+
const document = generateOpenApi(app, { info: { title: "Shop", version: "1.2.0" } })
|
|
812
|
+
const manifest = generateManifest(app, {})
|
|
813
|
+
const yaml = toYaml(document)
|
|
1474
814
|
|
|
1475
|
-
|
|
815
|
+
serveManifest(app, { path: "/manifest.json" })
|
|
1476
816
|
|
|
1477
|
-
|
|
817
|
+
/* the two reference pages, if you mount one yourself rather than through docs: */
|
|
818
|
+
const pages = [scalar({ title: "Shop", url: "/openapi.json" }), swagger({ title: "Shop", url: "/openapi.json" })]
|
|
819
|
+
```
|
|
1478
820
|
|
|
1479
|
-
|
|
821
|
+
Webhooks — the requests your API sends out — are declared with the document, signature and all, so a generated
|
|
822
|
+
client gets a typed parser and a verifier for them:
|
|
1480
823
|
|
|
1481
824
|
```ts
|
|
1482
|
-
import "@lovrozagar/honey
|
|
825
|
+
import { honey } from "@lovrozagar/honey"
|
|
826
|
+
import { serveOpenApi } from "@lovrozagar/honey/openapi"
|
|
827
|
+
import * as z from "zod"
|
|
1483
828
|
|
|
1484
|
-
const
|
|
829
|
+
const app = honey()
|
|
1485
830
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
})
|
|
1496
|
-
conn.on("close", () => {
|
|
1497
|
-
conn.publish(room, { left: conn.userId })
|
|
1498
|
-
})
|
|
831
|
+
serveOpenApi(app, {
|
|
832
|
+
title: "Shop",
|
|
833
|
+
version: "1.0.0",
|
|
834
|
+
webhooks: {
|
|
835
|
+
orderShipped: {
|
|
836
|
+
body: z.object({ orderId: z.string(), shippedAt: z.string() }),
|
|
837
|
+
signature: { algorithm: "hmac-sha256", encoding: "hex", header: "x-signature", prefix: "sha256=" },
|
|
838
|
+
summary: "An order left the warehouse",
|
|
839
|
+
},
|
|
1499
840
|
},
|
|
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
841
|
})
|
|
1508
842
|
```
|
|
1509
843
|
|
|
1510
|
-
|
|
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.
|
|
844
|
+
Full reference: [the OpenAPI document](https://github.com/lovrozagar/honey/blob/main/docs/openapi.md) and
|
|
845
|
+
[webhooks](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/webhooks.md).
|
|
1513
846
|
|
|
1514
|
-
|
|
847
|
+
## The MCP server
|
|
1515
848
|
|
|
1516
|
-
|
|
849
|
+
There are two ways to serve your API's tools over the Model Context Protocol, and they run in different places. The
|
|
850
|
+
standalone server `honey-clients generate --target mcp` writes needs Node, and is what a local stdio install
|
|
851
|
+
(Claude Desktop, Claude Code, Cursor) starts. The mounted endpoint below runs wherever the app runs — Workers
|
|
852
|
+
included — dispatches in process, and is guarded by the app's own auth. It is the one to use remotely.
|
|
1517
853
|
|
|
1518
|
-
|
|
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 |
|
|
854
|
+
`mcp(app, path, handler, options)` mounts the handler `codegen.mcp` generated beside your app:
|
|
1530
855
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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 |
|
|
856
|
+
```ts
|
|
857
|
+
import { honey } from "@lovrozagar/honey"
|
|
858
|
+
import { type McpHandler, mcp } from "@lovrozagar/honey/mcp"
|
|
1539
859
|
|
|
1540
|
-
|
|
860
|
+
/* in the app: import { mcpHandler } from "./_gen/mcp.gen.ts" */
|
|
861
|
+
declare const mcpHandler: McpHandler
|
|
1541
862
|
|
|
1542
|
-
|
|
863
|
+
const app = honey()
|
|
864
|
+
mcp(app, "/mcp", mcpHandler, {
|
|
865
|
+
authenticate: (request) => /^ApiKey \S+$/.test(request.headers.get("authorization") ?? ""),
|
|
866
|
+
})
|
|
867
|
+
```
|
|
1543
868
|
|
|
1544
|
-
|
|
869
|
+
Three opt-ins ride on the mount. `oauth()` makes the endpoint an OAuth 2.1 resource server (RFC 9728 metadata and
|
|
870
|
+
Bearer challenges), which is what ChatGPT requires; `uploads()` takes files out of band, so a large file never goes
|
|
871
|
+
through a tool argument; `urlFiles()` lets a file argument be an https URL from a host you list.
|
|
1545
872
|
|
|
1546
873
|
```ts
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
```
|
|
874
|
+
import { honey } from "@lovrozagar/honey"
|
|
875
|
+
import { type McpHandler, mcp } from "@lovrozagar/honey/mcp"
|
|
876
|
+
import { oauth } from "@lovrozagar/honey/mcp/oauth"
|
|
877
|
+
import { memoryUploadStore, uploads } from "@lovrozagar/honey/mcp/uploads"
|
|
878
|
+
import { urlFiles } from "@lovrozagar/honey/mcp/url-files"
|
|
1565
879
|
|
|
1566
|
-
|
|
880
|
+
/* in the app: import { mcpHandler } from "./_gen/mcp.gen.ts" */
|
|
881
|
+
declare const mcpHandler: McpHandler
|
|
882
|
+
declare const UPLOAD_SECRET: string
|
|
1567
883
|
|
|
1568
|
-
|
|
1569
|
-
app
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
884
|
+
const app = honey()
|
|
885
|
+
mcp(app, "/mcp", mcpHandler, {
|
|
886
|
+
authenticate: (request) => /^ApiKey \S+$/.test(request.headers.get("authorization") ?? ""),
|
|
887
|
+
oauth: oauth({
|
|
888
|
+
authorizationServers: ["https://auth.example.com"],
|
|
889
|
+
resource: "https://mcp.example.com/mcp",
|
|
890
|
+
resourceName: "Example",
|
|
891
|
+
scopesSupported: ["mcp:read"],
|
|
1576
892
|
}),
|
|
1577
|
-
)
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
-
)
|
|
893
|
+
uploads: uploads({ secret: UPLOAD_SECRET, store: memoryUploadStore() }),
|
|
894
|
+
urlFiles: urlFiles({ hosts: ["files.example.com"] }),
|
|
895
|
+
})
|
|
1592
896
|
```
|
|
1593
897
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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.
|
|
898
|
+
`memoryUploadStore()` is for one process; on Workers the store is yours (a Durable Object for the claim, R2 for the
|
|
899
|
+
bytes), and the MCP page shows that end to end. Only a route marked `meta({ mcp: true })` becomes a tool, and each
|
|
900
|
+
tool's input and output schemas come from the route's own.
|
|
1601
901
|
|
|
1602
|
-
|
|
902
|
+
Full reference: [the generated MCP server](https://github.com/lovrozagar/honey/blob/main/docs/mcp.md) and
|
|
903
|
+
[MCP structured output](https://github.com/lovrozagar/honey/blob/main/docs/capabilities/mcp-structured-output.md).
|
|
1603
904
|
|
|
1604
|
-
##
|
|
905
|
+
## Logging and tracing
|
|
1605
906
|
|
|
1606
|
-
|
|
907
|
+
`logger()` sets where lines go, `requestLog()` turns on the one line per request, and `onError` maps a thrown value
|
|
908
|
+
to a response before the error is reported.
|
|
1607
909
|
|
|
1608
|
-
|
|
910
|
+
```ts
|
|
911
|
+
import { HoneyError, honey } from "@lovrozagar/honey"
|
|
912
|
+
import { createLogger } from "@lovrozagar/honey/logger"
|
|
913
|
+
import { requestId } from "@lovrozagar/honey/request-id"
|
|
1609
914
|
|
|
1610
|
-
|
|
915
|
+
const app = honey()
|
|
916
|
+
.use(requestId())
|
|
917
|
+
.logger(createLogger({ level: "info" }))
|
|
918
|
+
.requestLog(true)
|
|
919
|
+
.onError((error, ctx) => {
|
|
920
|
+
if (error instanceof HoneyError && error.errorKey === "db_timeout") return ctx.jsonFromError(error)
|
|
921
|
+
return undefined
|
|
922
|
+
})
|
|
1611
923
|
|
|
1612
|
-
|
|
924
|
+
app.get("/").handler((ctx) => ctx.res.text("ok", "up"))
|
|
925
|
+
```
|
|
1613
926
|
|
|
1614
|
-
|
|
927
|
+
Tracing is an adapter: `otelAdapter` nests `http.request` → `middleware.*` → `http.handler` and continues an incoming
|
|
928
|
+
W3C `traceparent`, so a span from a caller stays one trace. With it on, the logger's child binds `trace_id` and
|
|
929
|
+
`span_id`, and `requestId` stays the client-facing id.
|
|
1615
930
|
|
|
1616
|
-
<!-- snippet:define client-app
|
|
1617
931
|
```ts
|
|
1618
932
|
import { honey } from "@lovrozagar/honey"
|
|
1619
|
-
import
|
|
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 -->
|
|
933
|
+
import { createLogger } from "@lovrozagar/honey/logger"
|
|
934
|
+
import { otelAdapter, type OtelApi } from "@lovrozagar/honey/telemetry/otel"
|
|
1636
935
|
|
|
1637
|
-
|
|
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
|
-
})
|
|
936
|
+
declare const api: OtelApi
|
|
1654
937
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
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" } })
|
|
938
|
+
const app = honey()
|
|
939
|
+
.logger(createLogger({ level: "info" }))
|
|
940
|
+
.telemetry(otelAdapter(api))
|
|
941
|
+
.requestLog(true)
|
|
1664
942
|
|
|
1665
|
-
|
|
1666
|
-
client.$path("/api/users/:id", { params: { id: "1" } })
|
|
1667
|
-
const ws = client.ws("/echo-ws", { reconnectToken: "t" })
|
|
943
|
+
app.get("/").handler((ctx) => ctx.res.text("ok", "up"))
|
|
1668
944
|
```
|
|
1669
945
|
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
Per-call options: `json`, `form`, `search`, `params`, `headers`, `cookies`, `timeout`, `signal`, `lastEventId`.
|
|
946
|
+
A request logged as the command that would repeat it is its own middleware, and the same formatter is available on
|
|
947
|
+
its own for a failing call you want to paste into a terminal — both redact credentials by default:
|
|
1673
948
|
|
|
1674
|
-
|
|
949
|
+
```ts
|
|
950
|
+
import { honey } from "@lovrozagar/honey"
|
|
951
|
+
import { curlLogger } from "@lovrozagar/honey/curl-logger"
|
|
952
|
+
import { requestToCurl } from "@lovrozagar/honey/request-to-curl"
|
|
1675
953
|
|
|
1676
|
-
|
|
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.
|
|
954
|
+
declare const req: Request
|
|
1680
955
|
|
|
1681
|
-
|
|
956
|
+
const app = honey().use(curlLogger())
|
|
957
|
+
app.get("/").handler((ctx) => ctx.res.text("ok", "up"))
|
|
1682
958
|
|
|
1683
|
-
|
|
959
|
+
/* the authorization header left out of the logged command */
|
|
960
|
+
const command = await requestToCurl(req, { excludeHeader: (name) => name === "authorization" })
|
|
961
|
+
```
|
|
1684
962
|
|
|
1685
|
-
|
|
963
|
+
Full reference: [logging, errors and tracing](https://github.com/lovrozagar/honey/blob/main/docs/observability.md).
|
|
1686
964
|
|
|
1687
|
-
|
|
965
|
+
## Internationalization and crypto
|
|
1688
966
|
|
|
1689
|
-
|
|
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
|
-
})
|
|
967
|
+
Error messages translate through a dictionary you supply, and ICU messages interpolate with plurals:
|
|
1706
968
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
credentials: "include",
|
|
1710
|
-
throwOnError: true,
|
|
1711
|
-
})
|
|
969
|
+
```ts run
|
|
970
|
+
import { interpolate } from "@lovrozagar/honey/i18n"
|
|
1712
971
|
|
|
1713
|
-
const
|
|
972
|
+
const message = "{count, plural, one {# new message} other {# new messages}} for {name}"
|
|
973
|
+
console.log(interpolate(message, { count: 3, name: "Ada" }, "en"))
|
|
974
|
+
// prints: 3 new messages for Ada
|
|
1714
975
|
```
|
|
1715
976
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
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.
|
|
977
|
+
```ts
|
|
978
|
+
import { defineErrors, honey } from "@lovrozagar/honey"
|
|
979
|
+
import { errorI18n, translateError } from "@lovrozagar/honey/i18n"
|
|
1720
980
|
|
|
1721
|
-
|
|
981
|
+
const errors = defineErrors({ not_found: "not_found" })
|
|
1722
982
|
|
|
1723
|
-
|
|
983
|
+
/* locale, then error key, then the ICU template that key's message reads from */
|
|
984
|
+
const translations = { errors: { de: { not_found: "Nicht gefunden" }, en: { not_found: "Not found" } } }
|
|
1724
985
|
|
|
1725
|
-
|
|
986
|
+
const app = honey().errorI18n(
|
|
987
|
+
errorI18n({ ...translations, resolveLocale: (ctx) => ctx.headers["accept-language"]?.slice(0, 2) ?? "en" }),
|
|
988
|
+
)
|
|
1726
989
|
|
|
1727
|
-
|
|
990
|
+
app.get("/books/:id").handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
|
|
1728
991
|
|
|
1729
|
-
|
|
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
|
-
}
|
|
992
|
+
const german = translateError(errors.not_found(), translations, "de")
|
|
1741
993
|
```
|
|
1742
994
|
|
|
1743
|
-
|
|
995
|
+
Crypto is WebCrypto, with the comparison that does not leak by timing:
|
|
1744
996
|
|
|
1745
|
-
|
|
997
|
+
```ts run
|
|
998
|
+
import { timingSafeEqual } from "@lovrozagar/honey/crypto"
|
|
1746
999
|
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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" })
|
|
1000
|
+
const expected = "s3cret-token"
|
|
1001
|
+
console.log(await timingSafeEqual("s3cret-token", expected), await timingSafeEqual("guess", expected))
|
|
1002
|
+
// prints: true false
|
|
1760
1003
|
```
|
|
1761
1004
|
|
|
1762
|
-
|
|
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
|
-
```
|
|
1005
|
+
Full reference: [internationalization](https://github.com/lovrozagar/honey/blob/main/docs/i18n.md) and
|
|
1006
|
+
[crypto](https://github.com/lovrozagar/honey/blob/main/docs/crypto.md).
|
|
1798
1007
|
|
|
1799
1008
|
## Testing
|
|
1800
1009
|
|
|
1801
|
-
|
|
1010
|
+
`testClient(app, options)` calls the app the way a client would, in process by default — no port, no server — and
|
|
1011
|
+
over real HTTP when you ask for it. Request fields a handler reads but a test cannot easily forge, such as `ip`, are
|
|
1012
|
+
options rather than mocks.
|
|
1013
|
+
|
|
1014
|
+
```ts run
|
|
1015
|
+
import { honey } from "@lovrozagar/honey"
|
|
1802
1016
|
import { testClient } from "@lovrozagar/honey/testing"
|
|
1803
1017
|
|
|
1804
|
-
const
|
|
1018
|
+
const app = honey()
|
|
1019
|
+
app.get("/hello").handler((ctx) => ctx.res.json("ok", { hello: ctx.search.name ?? "world", ip: ctx.ip }))
|
|
1805
1020
|
|
|
1806
|
-
const
|
|
1807
|
-
await client.
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
search: { dry: "1" },
|
|
1811
|
-
})
|
|
1812
|
-
await client.post("/login", { form: { user: "a", pass: "b" } })
|
|
1021
|
+
const client = testClient(app, { env: {} })
|
|
1022
|
+
const answer = await client.get("/hello", { search: { name: "Ada" }, ip: "203.0.113.7" })
|
|
1023
|
+
console.log(answer.status, JSON.stringify(await answer.json()))
|
|
1024
|
+
// prints: 200 {"hello":"Ada","ip":"203.0.113.7"}
|
|
1813
1025
|
```
|
|
1814
1026
|
|
|
1815
|
-
|
|
1027
|
+
Full reference: [testing an app](https://github.com/lovrozagar/honey/blob/main/docs/testing.md) and
|
|
1028
|
+
[SDK conformance](https://github.com/lovrozagar/honey/blob/main/docs/conformance.md), which is how the generated
|
|
1029
|
+
clients are held to behaving alike.
|
|
1816
1030
|
|
|
1817
|
-
|
|
1031
|
+
## The build plugin and the honey CLI
|
|
1818
1032
|
|
|
1819
|
-
|
|
1033
|
+
The Vite plugin writes the app's artifacts — the route tree, the types, the manifest, the OpenAPI document and the
|
|
1034
|
+
MCP handler — at build start, and again on each save in dev:
|
|
1820
1035
|
|
|
1821
1036
|
```ts
|
|
1822
|
-
import {
|
|
1823
|
-
|
|
1037
|
+
import { honey } from "@lovrozagar/honey/plugin"
|
|
1038
|
+
import { defineConfig } from "vite"
|
|
1824
1039
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1040
|
+
export default defineConfig({
|
|
1041
|
+
plugins: [honey({ app: "src/app.ts" })],
|
|
1042
|
+
})
|
|
1043
|
+
```
|
|
1827
1044
|
|
|
1828
|
-
|
|
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
|
|
1045
|
+
What it generates is `codegen`, and each output takes a path or a config of its own:
|
|
1832
1046
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1047
|
+
```ts
|
|
1048
|
+
import { honey } from "@lovrozagar/honey/plugin"
|
|
1835
1049
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1050
|
+
export default {
|
|
1051
|
+
plugins: [
|
|
1052
|
+
honey({
|
|
1053
|
+
app: "src/app.ts",
|
|
1054
|
+
codegen: {
|
|
1055
|
+
manifest: true,
|
|
1056
|
+
mcp: true,
|
|
1057
|
+
openApi: { title: "Shop", version: "1.0.0", securitySchemes: {} },
|
|
1058
|
+
tree: true,
|
|
1059
|
+
types: { baseCtxName: "AppCtx" },
|
|
1060
|
+
},
|
|
1061
|
+
watch: ["src/**/*.ts"],
|
|
1062
|
+
}),
|
|
1063
|
+
],
|
|
1064
|
+
}
|
|
1838
1065
|
```
|
|
1839
1066
|
|
|
1840
|
-
|
|
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
|
|
1067
|
+
`@lovrozagar/honey/build` adds the production bundle for a deploy target, so `vite build` emits one file to run:
|
|
1876
1068
|
|
|
1877
|
-
```
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
-
```
|
|
1069
|
+
```ts
|
|
1070
|
+
import { createBuildPlugin } from "@lovrozagar/honey/build"
|
|
1071
|
+
import { defineConfig } from "vite"
|
|
1884
1072
|
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
|
1073
|
+
export default defineConfig({
|
|
1074
|
+
plugins: [createBuildPlugin({ target: "cloudflare", minify: true }, { entry: "src/app.ts", export: "app" })],
|
|
1075
|
+
})
|
|
1909
1076
|
```
|
|
1910
1077
|
|
|
1911
|
-
|
|
1078
|
+
The `honey` CLI does the same generation without Vite, which is what a CI step or a `package.json` script wants:
|
|
1912
1079
|
|
|
1913
|
-
```
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1080
|
+
```sh
|
|
1081
|
+
honey generate # reads vite.config.* and the honey() plugin in it
|
|
1082
|
+
honey generate --app src/app.ts --tree --types --manifest
|
|
1083
|
+
honey generate --config vite.config.ts --plugin 0 --watch
|
|
1084
|
+
honey generate --merge-tree src/_gen/routes.gen.ts # a gateway's merged tree
|
|
1085
|
+
honey init --cf --force
|
|
1917
1086
|
```
|
|
1918
1087
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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
|
-
```
|
|
1088
|
+
| Flag | What it does |
|
|
1089
|
+
| --------------------- | -------------------------------------------------------------------------- |
|
|
1090
|
+
| `--config <path>` | the Vite config holding `honey()` (default: `vite.config.{ts,mts,js,mjs}`) |
|
|
1091
|
+
| `--plugin <n>` | which `honey()` plugin in the config, 0-based |
|
|
1092
|
+
| `--app <path>` | the app entry; overrides the config's app, or replaces the config |
|
|
1093
|
+
| `--tree` | the route tree module |
|
|
1094
|
+
| `--types` | the ambient types |
|
|
1095
|
+
| `--manifest` | the route manifest |
|
|
1096
|
+
| `--merge-tree <path>` | the route tree source of a gateway |
|
|
1097
|
+
| `--watch` | generate again when files next to the app change |
|
|
1950
1098
|
|
|
1951
|
-
The
|
|
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.
|
|
1099
|
+
The SDKs, the CLI and the MCP server are honey-clients's: `honey-clients generate --app <path>`.
|
|
1957
1100
|
|
|
1958
|
-
|
|
1101
|
+
Full reference: [the build plugin](https://github.com/lovrozagar/honey/blob/main/docs/plugin.md).
|
|
1959
1102
|
|
|
1960
|
-
|
|
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 |
|
|
1103
|
+
## Generated clients
|
|
1967
1104
|
|
|
1968
|
-
|
|
1105
|
+
`honey-clients` reads the app module or the document and writes clients that know the routes:
|
|
1969
1106
|
|
|
1970
|
-
|
|
1107
|
+
```sh
|
|
1108
|
+
npx honey-clients generate --app src/app.ts --target ts --target python --out generated
|
|
1109
|
+
```
|
|
1971
1110
|
|
|
1972
|
-
|
|
1111
|
+
A call answers your value and throws a typed error by status; its `client.safe` mirror answers the same calls as a
|
|
1112
|
+
`Result` instead, so a failure is a value rather than a throw. All six SDKs have both styles.
|
|
1973
1113
|
|
|
1974
|
-
|
|
1114
|
+
```ts sdk
|
|
1115
|
+
import { DecodeError, Kitchen, NotFoundError } from "./kitchen"
|
|
1975
1116
|
|
|
1976
|
-
|
|
1117
|
+
export function newClient(): Kitchen {
|
|
1118
|
+
return new Kitchen({
|
|
1119
|
+
baseURL: "https://api.example.com",
|
|
1120
|
+
headers: { authorization: `Bearer ${process.env.KITCHEN_TOKEN ?? ""}` },
|
|
1121
|
+
timeout: 10_000,
|
|
1122
|
+
})
|
|
1123
|
+
}
|
|
1977
1124
|
|
|
1978
|
-
|
|
1125
|
+
export async function organization(client: Kitchen, orgId: string): Promise<string | null> {
|
|
1126
|
+
try {
|
|
1127
|
+
await client.getApiV1OrganizationsByOrgId({ params: { orgId } })
|
|
1128
|
+
return orgId
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
if (error instanceof NotFoundError) return null
|
|
1131
|
+
throw error
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1979
1134
|
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1135
|
+
export async function health(client: Kitchen): Promise<string> {
|
|
1136
|
+
const result = await client.safe.getApiHealth()
|
|
1137
|
+
if (result.ok) return "up"
|
|
1138
|
+
if (result.error instanceof DecodeError) return `unreadable answer: ${result.error.message}`
|
|
1139
|
+
return `answered ${result.error.status}`
|
|
1140
|
+
}
|
|
1984
1141
|
```
|
|
1985
1142
|
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1143
|
+
That block is a verbatim slice of
|
|
1144
|
+
[the compiled example](https://github.com/lovrozagar/honey/blob/main/docs/examples/ts/example.ts): the tests
|
|
1145
|
+
type-check it against an SDK generated from a real document, so it cannot drift from what the generator writes.
|
|
1146
|
+
|
|
1147
|
+
Full reference: [code generation](https://github.com/lovrozagar/honey/blob/main/docs/codegen.md),
|
|
1148
|
+
[the generated clients](https://github.com/lovrozagar/honey/blob/main/docs/client.md),
|
|
1149
|
+
[their per-language recipes](https://github.com/lovrozagar/honey/blob/main/docs/sdk-recipes.md),
|
|
1150
|
+
[the generated CLI](https://github.com/lovrozagar/honey/blob/main/docs/cli.md) and
|
|
1151
|
+
[releasing it](https://github.com/lovrozagar/honey/blob/main/docs/cli-release.md).
|
|
1152
|
+
|
|
1153
|
+
## Every export
|
|
1154
|
+
|
|
1155
|
+
Each feature is a subpath: import what the app uses, and a bundle carries nothing else. An app that never imports
|
|
1156
|
+
`@lovrozagar/honey/ws` bundles no WebSocket code, and one that never imports `@lovrozagar/honey/openapi` no document
|
|
1157
|
+
generator.
|
|
1158
|
+
|
|
1159
|
+
| Export | What it holds |
|
|
1160
|
+
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1161
|
+
| `@lovrozagar/honey` | `honey`, `HoneyError`, `defineErrors`, `createMiddleware`, `defineMiddleware`, `mergeTree`, `detectRuntime`, and the type helpers below |
|
|
1162
|
+
| `@lovrozagar/honey/serve` | `serve(app, options)`: Node or Bun, whichever runs it |
|
|
1163
|
+
| `@lovrozagar/honey/node` | `serve` for `node:http` directly |
|
|
1164
|
+
| `@lovrozagar/honey/ws` | `ws(app, path)`, a WebSocket route |
|
|
1165
|
+
| `@lovrozagar/honey/ws/bun` | `bunWebSocket` |
|
|
1166
|
+
| `@lovrozagar/honey/ws/node` | `nodeWebSocket` (with the optional `ws` peer) |
|
|
1167
|
+
| `@lovrozagar/honey/ws/cloudflare` | `cfWebSocket` |
|
|
1168
|
+
| `@lovrozagar/honey/ws/session` | `createWsSession`: the session runtime a custom WebSocket adapter builds on |
|
|
1169
|
+
| `@lovrozagar/honey/realtime` | `realtime(app, path, options)`: a resumable connection over WebSocket, SSE and long-poll |
|
|
1170
|
+
| `@lovrozagar/honey/mcp` | `mcp(app, path, handler, options?)`: the app's tools over MCP's Streamable HTTP, the handler `codegen.mcp` writes |
|
|
1171
|
+
| `@lovrozagar/honey/mcp/oauth` | `oauth(options)`: `mcp()`'s opt-in OAuth 2.1 resource server (RFC 9728 metadata, Bearer challenges) |
|
|
1172
|
+
| `@lovrozagar/honey/mcp/uploads` | `uploads(options)`, `memoryUploadStore()`: `mcp()`'s opt-in out-of-band file uploads (the file-uploads extension's flow) |
|
|
1173
|
+
| `@lovrozagar/honey/mcp/url-files` | `urlFiles(options)`: `mcp()`'s opt-in file arguments as https URLs from listed hosts |
|
|
1174
|
+
| `@lovrozagar/honey/openapi` | `serveOpenApi`, `serveManifest`, `generateOpenApi`, `generateManifest`, `toYaml` |
|
|
1175
|
+
| `@lovrozagar/honey/openapi/scalar` | `scalar`, the Scalar API reference page |
|
|
1176
|
+
| `@lovrozagar/honey/openapi/swagger` | `swagger`, the Swagger UI page |
|
|
1177
|
+
| `@lovrozagar/honey/errors` | `defineErrors` (also on the root) |
|
|
1178
|
+
| `@lovrozagar/honey/i18n` | `errorI18n`, `translateError`, `interpolate`, `createInterpolator` |
|
|
1179
|
+
| `@lovrozagar/honey/input` | `readableStream()`, a streamed request body as input |
|
|
1180
|
+
| `@lovrozagar/honey/range` | `rangeResponse`, a `Range` request answered from bytes |
|
|
1181
|
+
| `@lovrozagar/honey/cache-control` | `cacheControl`, `httpDate` |
|
|
1182
|
+
| `@lovrozagar/honey/compress` | `compress` |
|
|
1183
|
+
| `@lovrozagar/honey/etag` | `etag`, `notModified` |
|
|
1184
|
+
| `@lovrozagar/honey/rate-limit` | `rateLimit`, `memoryStore` |
|
|
1185
|
+
| `@lovrozagar/honey/cors` | `cors` |
|
|
1186
|
+
| `@lovrozagar/honey/csrf` | `csrf` |
|
|
1187
|
+
| `@lovrozagar/honey/body-limit` | `bodyLimit` |
|
|
1188
|
+
| `@lovrozagar/honey/timeout` | `timeout` |
|
|
1189
|
+
| `@lovrozagar/honey/ip-restrict` | `ipRestrict` |
|
|
1190
|
+
| `@lovrozagar/honey/request-id` | `requestId` |
|
|
1191
|
+
| `@lovrozagar/honey/secure-headers` | `secureHeaders` |
|
|
1192
|
+
| `@lovrozagar/honey/server-timing` | `serverTiming` |
|
|
1193
|
+
| `@lovrozagar/honey/powered-by` | `poweredBy` |
|
|
1194
|
+
| `@lovrozagar/honey/pretty-json` | `prettyJson` |
|
|
1195
|
+
| `@lovrozagar/honey/logger` | `createLogger` |
|
|
1196
|
+
| `@lovrozagar/honey/curl-logger` | `curlLogger`, a request logged as a command that repeats it |
|
|
1197
|
+
| `@lovrozagar/honey/telemetry/otel` | `otelAdapter` |
|
|
1198
|
+
| `@lovrozagar/honey/static` | `staticFiles` |
|
|
1199
|
+
| `@lovrozagar/honey/proxy` | `proxy`, a route that forwards to an upstream |
|
|
1200
|
+
| `@lovrozagar/honey/accepts` | `accepts`, content negotiation |
|
|
1201
|
+
| `@lovrozagar/honey/cookie` | `serializeCookie`, `parseCookieHeader` |
|
|
1202
|
+
| `@lovrozagar/honey/cookie-sign` | `sign`, `verify`, `createCookieSigner` |
|
|
1203
|
+
| `@lovrozagar/honey/crypto` | `timingSafeEqual` |
|
|
1204
|
+
| `@lovrozagar/honey/request-to-curl` | `requestToCurl` |
|
|
1205
|
+
| `@lovrozagar/honey/testing` | `testClient`, an app called in process or over `serve` |
|
|
1206
|
+
| `@lovrozagar/honey/tree` | the route tree's types and helpers |
|
|
1207
|
+
| `@lovrozagar/honey/plugin` | `honey()`, the Vite plugin that generates the app's artifacts |
|
|
1208
|
+
| `@lovrozagar/honey/build` | `createBuildPlugin`, a production bundle for a deploy target |
|
|
1209
|
+
|
|
1210
|
+
The root export's values: `honey()` and the `Honey` class behind it, `HoneyContext` and `HoneyRes` (the handler's
|
|
1211
|
+
`ctx` and its `ctx.res`), `HoneyError`, `defineErrors` and `ERROR_META`, `createErrorResponse`, `createMiddleware`
|
|
1212
|
+
and `defineMiddleware`, `mergeTree`, `detectRuntime` and `toFetchRequest`.
|
|
1213
|
+
|
|
1214
|
+
## Type helpers
|
|
1215
|
+
|
|
1216
|
+
Exported from `@lovrozagar/honey`, each over the app you built (`typeof app`). This is how a test, a client or a
|
|
1217
|
+
helper of yours stays tied to the routes rather than to a copy of their shapes.
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
import { honey, type InferRouteInput, type InferRoutePaths, type InferRoutes } from "@lovrozagar/honey"
|
|
1221
|
+
import * as z from "zod"
|
|
1995
1222
|
|
|
1996
|
-
|
|
1223
|
+
const app = honey()
|
|
1224
|
+
.get("/books/:id")
|
|
1225
|
+
.input({ params: z.object({ id: z.string() }) })
|
|
1226
|
+
.handler((ctx) => ctx.res.json("ok", { id: ctx.input.params.id }))
|
|
1227
|
+
|
|
1228
|
+
type Paths = InferRoutePaths<typeof app>
|
|
1229
|
+
type Routes = InferRoutes<typeof app>
|
|
1230
|
+
type BookInput = InferRouteInput<typeof app, "/books/:id", "get">
|
|
1231
|
+
|
|
1232
|
+
const path: Paths = "/books/:id"
|
|
1233
|
+
declare const input: BookInput
|
|
1234
|
+
const id: string = input.params.id
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
| Type | What it describes |
|
|
1238
|
+
| -------------------------------------------- | ---------------------------------------------------------------- |
|
|
1239
|
+
| `InferRoutes<typeof app>` | every path, its methods, and each one's input, output and errors |
|
|
1240
|
+
| `InferRoutePaths<typeof app>` | the paths, as a union |
|
|
1241
|
+
| `InferRouteMethods<typeof app, Path>` | one path's methods |
|
|
1242
|
+
| `InferRouteInput<typeof app, Path, Method>` | a route's validated input |
|
|
1243
|
+
| `InferRouteOutput<typeof app, Path, Method>` | a route's output map |
|
|
1244
|
+
| `InferRouteErrors<typeof app, Path, Method>` | a route's declared error keys |
|
|
1245
|
+
| `InferRouteMeta<typeof app, Path, Method>` | a route's meta |
|
|
1246
|
+
| `InferRouteCtx<typeof app, Path, Method>` | a route's handler `ctx` |
|
|
1247
|
+
| `InferCtx<typeof app>` | the app's handler context |
|
|
1248
|
+
| `InferEnv<typeof app>` | `ctx.env` |
|
|
1249
|
+
| `InferMeta<typeof app>` | the app-level meta |
|
|
1250
|
+
| `InferErrorFactory<typeof app>` | the error factory |
|
|
1251
|
+
| `InferBasePath<typeof app>` | the base path, as a string |
|
|
1252
|
+
| `InferMethods<typeof app>` | the methods the app serves |
|
|
1253
|
+
|
|
1254
|
+
## Documentation
|
|
1255
|
+
|
|
1256
|
+
Every page's examples are compiled by the repository's tests.
|
|
1257
|
+
|
|
1258
|
+
- [All the docs](https://github.com/lovrozagar/honey/blob/main/docs/README.md)
|
|
1259
|
+
- [Migrating from 0.7](https://github.com/lovrozagar/honey/blob/main/docs/migration.md)
|
|
1260
|
+
- [Package exports](https://github.com/lovrozagar/honey/blob/main/docs/packages.md)
|
|
1261
|
+
- [What a tarball ships, and why](https://github.com/lovrozagar/honey/blob/main/packages/core/docs/packaging.md)
|
|
1262
|
+
- [The repository](https://github.com/lovrozagar/honey)
|
|
1997
1263
|
|
|
1998
1264
|
## License
|
|
1999
1265
|
|