@lovrozagar/honey 0.7.2 → 1.0.0-rc.1

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