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