@lovrozagar/honey 0.6.8 → 0.7.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 (674) hide show
  1. package/README.md +514 -192
  2. package/bin/honey.js +25 -0
  3. package/dist/accepts.d.ts +7 -1
  4. package/dist/accepts.d.ts.map +1 -1
  5. package/dist/accepts.js +126 -0
  6. package/dist/accepts.js.map +1 -0
  7. package/dist/body-kind.d.ts +26 -0
  8. package/dist/body-kind.d.ts.map +1 -0
  9. package/dist/body-kind.js +70 -0
  10. package/dist/body-kind.js.map +1 -0
  11. package/dist/body-limit.d.ts +5 -1
  12. package/dist/body-limit.d.ts.map +1 -1
  13. package/dist/body-limit.js +126 -0
  14. package/dist/body-limit.js.map +1 -0
  15. package/dist/build/index.d.ts +19 -3
  16. package/dist/build/index.d.ts.map +1 -1
  17. package/dist/build/index.js +173 -0
  18. package/dist/build/index.js.map +1 -0
  19. package/dist/cli-args.d.ts +33 -0
  20. package/dist/cli-args.d.ts.map +1 -0
  21. package/dist/cli-args.js +102 -0
  22. package/dist/cli-args.js.map +1 -0
  23. package/dist/cli-go/auth.go +26 -0
  24. package/dist/cli-go/config.go +192 -0
  25. package/dist/cli-go/errors.go +75 -0
  26. package/dist/cli-go/multipart.go +79 -0
  27. package/dist/cli-go/output.go +163 -0
  28. package/dist/cli-go/root.go +30 -0
  29. package/dist/cli-go/stream.go +88 -0
  30. package/dist/cli-go/version.go +5 -0
  31. package/dist/cli.d.ts +1 -1
  32. package/dist/cli.js +209 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/client/defaults.js +48 -0
  35. package/dist/client/defaults.js.map +1 -0
  36. package/dist/client/error.d.ts +35 -0
  37. package/dist/client/error.d.ts.map +1 -1
  38. package/dist/client/error.js +71 -0
  39. package/dist/client/error.js.map +1 -0
  40. package/dist/client/http.d.ts +67 -8
  41. package/dist/client/http.d.ts.map +1 -1
  42. package/dist/client/http.js +571 -0
  43. package/dist/client/http.js.map +1 -0
  44. package/dist/client/index.d.ts +3 -2
  45. package/dist/client/index.d.ts.map +1 -1
  46. package/dist/client/index.js +95 -0
  47. package/dist/client/index.js.map +1 -0
  48. package/dist/client/path.d.ts +26 -0
  49. package/dist/client/path.d.ts.map +1 -0
  50. package/dist/client/path.js +163 -0
  51. package/dist/client/path.js.map +1 -0
  52. package/dist/client/redirect-policy.d.ts +12 -0
  53. package/dist/client/redirect-policy.d.ts.map +1 -0
  54. package/dist/client/redirect-policy.js +20 -0
  55. package/dist/client/redirect-policy.js.map +1 -0
  56. package/dist/client/sdk.d.ts +36 -0
  57. package/dist/client/sdk.d.ts.map +1 -1
  58. package/dist/client/sdk.js +292 -0
  59. package/dist/client/sdk.js.map +1 -0
  60. package/dist/client/sse.d.ts +13 -0
  61. package/dist/client/sse.d.ts.map +1 -1
  62. package/dist/client/sse.js +129 -0
  63. package/dist/client/sse.js.map +1 -0
  64. package/dist/client/types.js +2 -0
  65. package/dist/client/types.js.map +1 -0
  66. package/dist/client/ws.d.ts +9 -2
  67. package/dist/client/ws.d.ts.map +1 -1
  68. package/dist/client/ws.js +112 -0
  69. package/dist/client/ws.js.map +1 -0
  70. package/dist/client-go/errors.go +236 -0
  71. package/dist/client-go/export.go +44 -0
  72. package/dist/client-go/invalidation.go +371 -0
  73. package/dist/client-go/nullable.go +62 -0
  74. package/dist/client-go/realtime.go +354 -0
  75. package/dist/client-go/result.go +20 -0
  76. package/dist/client-go/runtime.go +975 -0
  77. package/dist/client-go/sse.go +159 -0
  78. package/dist/client-go/transport.go +495 -0
  79. package/dist/client-go/ws.go +128 -0
  80. package/dist/client-info.d.ts +36 -0
  81. package/dist/client-info.d.ts.map +1 -0
  82. package/dist/client-info.js +53 -0
  83. package/dist/client-info.js.map +1 -0
  84. package/dist/client-mcp/README.md +10 -0
  85. package/dist/client-mcp/server.ts +221 -0
  86. package/dist/client-mcp/types.ts +19 -0
  87. package/dist/client-python/_errors.py +115 -0
  88. package/dist/client-python/_invalidation.py +494 -0
  89. package/dist/client-python/_realtime.py +179 -0
  90. package/dist/client-python/_runtime.py +860 -0
  91. package/dist/client-python/_sse.py +155 -0
  92. package/dist/client-python/_transport.py +366 -0
  93. package/dist/client-python/_ws.py +159 -0
  94. package/dist/client-python/py.typed +0 -0
  95. package/dist/client-rust/errors.rs +220 -0
  96. package/dist/client-rust/invalidation.rs +412 -0
  97. package/dist/client-rust/invalidation_sync.rs +93 -0
  98. package/dist/client-rust/nullable.rs +17 -0
  99. package/dist/client-rust/realtime.rs +702 -0
  100. package/dist/client-rust/result.rs +22 -0
  101. package/dist/client-rust/runtime.rs +980 -0
  102. package/dist/client-rust/runtime_sync.rs +484 -0
  103. package/dist/client-rust/sse.rs +188 -0
  104. package/dist/client-rust/ws.rs +167 -0
  105. package/dist/codegen-go-cli.d.ts.map +1 -1
  106. package/dist/codegen-go-cli.js +964 -0
  107. package/dist/codegen-go-cli.js.map +1 -0
  108. package/dist/codegen-go.d.ts +15 -3
  109. package/dist/codegen-go.d.ts.map +1 -1
  110. package/dist/codegen-go.js +872 -0
  111. package/dist/codegen-go.js.map +1 -0
  112. package/dist/codegen-hash.js +9 -0
  113. package/dist/codegen-hash.js.map +1 -0
  114. package/dist/codegen-ir.d.ts +69 -2
  115. package/dist/codegen-ir.d.ts.map +1 -1
  116. package/dist/codegen-ir.js +661 -0
  117. package/dist/codegen-ir.js.map +1 -0
  118. package/dist/codegen-lang.d.ts +65 -0
  119. package/dist/codegen-lang.d.ts.map +1 -0
  120. package/dist/codegen-lang.js +526 -0
  121. package/dist/codegen-lang.js.map +1 -0
  122. package/dist/codegen-load.d.ts +31 -0
  123. package/dist/codegen-load.d.ts.map +1 -0
  124. package/dist/codegen-load.js +138 -0
  125. package/dist/codegen-load.js.map +1 -0
  126. package/dist/codegen-loaders.js +55 -0
  127. package/dist/codegen-loaders.js.map +1 -0
  128. package/dist/codegen-mcp.d.ts.map +1 -1
  129. package/dist/codegen-mcp.js +368 -0
  130. package/dist/codegen-mcp.js.map +1 -0
  131. package/dist/codegen-python.d.ts +13 -1
  132. package/dist/codegen-python.d.ts.map +1 -1
  133. package/dist/codegen-python.js +828 -0
  134. package/dist/codegen-python.js.map +1 -0
  135. package/dist/codegen-route-tree-intern.d.ts +15 -5
  136. package/dist/codegen-route-tree-intern.d.ts.map +1 -1
  137. package/dist/codegen-route-tree-intern.js +221 -0
  138. package/dist/codegen-route-tree-intern.js.map +1 -0
  139. package/dist/codegen-rust.d.ts.map +1 -1
  140. package/dist/codegen-rust.js +1293 -0
  141. package/dist/codegen-rust.js.map +1 -0
  142. package/dist/codegen-sanitize.d.ts.map +1 -1
  143. package/dist/codegen-sanitize.js +56 -0
  144. package/dist/codegen-sanitize.js.map +1 -0
  145. package/dist/codegen-schema-naming.d.ts +5 -0
  146. package/dist/codegen-schema-naming.d.ts.map +1 -1
  147. package/dist/codegen-schema-naming.js +150 -0
  148. package/dist/codegen-schema-naming.js.map +1 -0
  149. package/dist/codegen-sdk-model.d.ts +71 -0
  150. package/dist/codegen-sdk-model.d.ts.map +1 -0
  151. package/dist/codegen-sdk-model.js +179 -0
  152. package/dist/codegen-sdk-model.js.map +1 -0
  153. package/dist/codegen.d.ts +41 -23
  154. package/dist/codegen.d.ts.map +1 -1
  155. package/dist/codegen.js +4610 -0
  156. package/dist/codegen.js.map +1 -0
  157. package/dist/context.d.ts +36 -6
  158. package/dist/context.d.ts.map +1 -1
  159. package/dist/context.js +242 -0
  160. package/dist/context.js.map +1 -0
  161. package/dist/cookie-sign.d.ts +16 -2
  162. package/dist/cookie-sign.d.ts.map +1 -1
  163. package/dist/cookie-sign.js +104 -0
  164. package/dist/cookie-sign.js.map +1 -0
  165. package/dist/cookie.d.ts +12 -0
  166. package/dist/cookie.d.ts.map +1 -1
  167. package/dist/cookie.js +138 -0
  168. package/dist/cookie.js.map +1 -0
  169. package/dist/cors.d.ts +1 -0
  170. package/dist/cors.d.ts.map +1 -1
  171. package/dist/cors.js +87 -0
  172. package/dist/cors.js.map +1 -0
  173. package/dist/crypto.d.ts +6 -1
  174. package/dist/crypto.d.ts.map +1 -1
  175. package/dist/crypto.js +38 -0
  176. package/dist/crypto.js.map +1 -0
  177. package/dist/csrf.d.ts +19 -0
  178. package/dist/csrf.d.ts.map +1 -1
  179. package/dist/csrf.js +79 -0
  180. package/dist/csrf.js.map +1 -0
  181. package/dist/curl-logger.d.ts +14 -3
  182. package/dist/curl-logger.d.ts.map +1 -1
  183. package/dist/curl-logger.js +199 -0
  184. package/dist/curl-logger.js.map +1 -0
  185. package/dist/detect-runtime.js +13 -0
  186. package/dist/detect-runtime.js.map +1 -0
  187. package/dist/dict.d.ts +11 -0
  188. package/dist/dict.d.ts.map +1 -0
  189. package/dist/dict.js +13 -0
  190. package/dist/dict.js.map +1 -0
  191. package/dist/error.d.ts.map +1 -1
  192. package/dist/error.js +44 -0
  193. package/dist/error.js.map +1 -0
  194. package/dist/errors.js +39 -0
  195. package/dist/errors.js.map +1 -0
  196. package/dist/etag.d.ts +2 -0
  197. package/dist/etag.d.ts.map +1 -1
  198. package/dist/etag.js +82 -0
  199. package/dist/etag.js.map +1 -0
  200. package/dist/feature-detect.d.ts +25 -0
  201. package/dist/feature-detect.d.ts.map +1 -0
  202. package/dist/feature-detect.js +144 -0
  203. package/dist/feature-detect.js.map +1 -0
  204. package/dist/feature-load.js +36 -0
  205. package/dist/feature-load.js.map +1 -0
  206. package/dist/feature-slots.d.ts +44 -0
  207. package/dist/feature-slots.d.ts.map +1 -0
  208. package/dist/feature-slots.js +48 -0
  209. package/dist/feature-slots.js.map +1 -0
  210. package/dist/fetch-request.d.ts +10 -0
  211. package/dist/fetch-request.d.ts.map +1 -0
  212. package/dist/fetch-request.js +13 -0
  213. package/dist/fetch-request.js.map +1 -0
  214. package/dist/gen-process.d.ts +15 -0
  215. package/dist/gen-process.d.ts.map +1 -0
  216. package/dist/gen-process.js +43 -0
  217. package/dist/gen-process.js.map +1 -0
  218. package/dist/gen-write.d.ts +19 -0
  219. package/dist/gen-write.d.ts.map +1 -0
  220. package/dist/gen-write.js +120 -0
  221. package/dist/gen-write.js.map +1 -0
  222. package/dist/glob.d.ts +21 -0
  223. package/dist/glob.d.ts.map +1 -0
  224. package/dist/glob.js +115 -0
  225. package/dist/glob.js.map +1 -0
  226. package/dist/go-type-emitter.d.ts +18 -4
  227. package/dist/go-type-emitter.d.ts.map +1 -1
  228. package/dist/go-type-emitter.js +594 -0
  229. package/dist/go-type-emitter.js.map +1 -0
  230. package/dist/honey-response.d.ts.map +1 -1
  231. package/dist/honey-response.js +338 -0
  232. package/dist/honey-response.js.map +1 -0
  233. package/dist/i18n-slot.d.ts +8 -0
  234. package/dist/i18n-slot.d.ts.map +1 -1
  235. package/dist/i18n-slot.js +14 -0
  236. package/dist/i18n-slot.js.map +1 -0
  237. package/dist/i18n.d.ts +3 -0
  238. package/dist/i18n.d.ts.map +1 -1
  239. package/dist/i18n.js +250 -0
  240. package/dist/i18n.js.map +1 -0
  241. package/dist/index.d.ts +408 -148
  242. package/dist/index.d.ts.map +1 -1
  243. package/dist/index.js +2324 -0
  244. package/dist/index.js.map +1 -0
  245. package/dist/init.d.ts +3 -5
  246. package/dist/init.d.ts.map +1 -1
  247. package/dist/init.js +140 -0
  248. package/dist/init.js.map +1 -0
  249. package/dist/input.js +4 -0
  250. package/dist/input.js.map +1 -0
  251. package/dist/invalidate-check.d.ts +11 -0
  252. package/dist/invalidate-check.d.ts.map +1 -1
  253. package/dist/invalidate-check.js +163 -0
  254. package/dist/invalidate-check.js.map +1 -0
  255. package/dist/invoke-user.d.ts +35 -0
  256. package/dist/invoke-user.d.ts.map +1 -0
  257. package/dist/invoke-user.js +73 -0
  258. package/dist/invoke-user.js.map +1 -0
  259. package/dist/ip-restrict.d.ts +13 -1
  260. package/dist/ip-restrict.d.ts.map +1 -1
  261. package/dist/ip-restrict.js +60 -0
  262. package/dist/ip-restrict.js.map +1 -0
  263. package/dist/ip.d.ts +39 -0
  264. package/dist/ip.d.ts.map +1 -0
  265. package/dist/ip.js +209 -0
  266. package/dist/ip.js.map +1 -0
  267. package/dist/logger.d.ts.map +1 -1
  268. package/dist/logger.js +113 -0
  269. package/dist/logger.js.map +1 -0
  270. package/dist/media-type.d.ts +30 -0
  271. package/dist/media-type.d.ts.map +1 -0
  272. package/dist/media-type.js +97 -0
  273. package/dist/media-type.js.map +1 -0
  274. package/dist/meta-spec-merge.d.ts +32 -0
  275. package/dist/meta-spec-merge.d.ts.map +1 -0
  276. package/dist/meta-spec-merge.js +68 -0
  277. package/dist/meta-spec-merge.js.map +1 -0
  278. package/dist/meta-spec.d.ts +16 -3
  279. package/dist/meta-spec.d.ts.map +1 -1
  280. package/dist/meta-spec.js +535 -0
  281. package/dist/meta-spec.js.map +1 -0
  282. package/dist/middleware.d.ts +23 -6
  283. package/dist/middleware.d.ts.map +1 -1
  284. package/dist/middleware.js +253 -0
  285. package/dist/middleware.js.map +1 -0
  286. package/dist/node-request.d.ts +57 -12
  287. package/dist/node-request.d.ts.map +1 -1
  288. package/dist/node-request.js +407 -0
  289. package/dist/node-request.js.map +1 -0
  290. package/dist/node.d.ts +20 -3
  291. package/dist/node.d.ts.map +1 -1
  292. package/dist/node.js +396 -0
  293. package/dist/node.js.map +1 -0
  294. package/dist/openapi/collect.d.ts +6 -3
  295. package/dist/openapi/collect.d.ts.map +1 -1
  296. package/dist/openapi/collect.js +30 -0
  297. package/dist/openapi/collect.js.map +1 -0
  298. package/dist/openapi/docs-page.d.ts +42 -0
  299. package/dist/openapi/docs-page.d.ts.map +1 -0
  300. package/dist/openapi/docs-page.js +80 -0
  301. package/dist/openapi/docs-page.js.map +1 -0
  302. package/dist/openapi/document.d.ts +4 -0
  303. package/dist/openapi/document.d.ts.map +1 -1
  304. package/dist/openapi/document.js +785 -0
  305. package/dist/openapi/document.js.map +1 -0
  306. package/dist/openapi/json-schema-slot.d.ts +7 -0
  307. package/dist/openapi/json-schema-slot.d.ts.map +1 -1
  308. package/dist/openapi/json-schema-slot.js +38 -0
  309. package/dist/openapi/json-schema-slot.js.map +1 -0
  310. package/dist/openapi/register.d.ts.map +1 -1
  311. package/dist/openapi/register.js +20 -0
  312. package/dist/openapi/register.js.map +1 -0
  313. package/dist/openapi/scalar.d.ts +3 -1
  314. package/dist/openapi/scalar.d.ts.map +1 -1
  315. package/dist/openapi/scalar.js +21 -0
  316. package/dist/openapi/scalar.js.map +1 -0
  317. package/dist/openapi/serve.d.ts +9 -0
  318. package/dist/openapi/serve.d.ts.map +1 -0
  319. package/dist/openapi/serve.js +36 -0
  320. package/dist/openapi/serve.js.map +1 -0
  321. package/dist/openapi/spec-factory.d.ts +30 -1
  322. package/dist/openapi/spec-factory.d.ts.map +1 -1
  323. package/dist/openapi/spec-factory.js +52 -0
  324. package/dist/openapi/spec-factory.js.map +1 -0
  325. package/dist/openapi/spec.d.ts +14 -5
  326. package/dist/openapi/spec.d.ts.map +1 -1
  327. package/dist/openapi/spec.js +50 -0
  328. package/dist/openapi/spec.js.map +1 -0
  329. package/dist/openapi/swagger.d.ts +3 -1
  330. package/dist/openapi/swagger.d.ts.map +1 -1
  331. package/dist/openapi/swagger.js +22 -0
  332. package/dist/openapi/swagger.js.map +1 -0
  333. package/dist/pattern.d.ts +71 -0
  334. package/dist/pattern.d.ts.map +1 -0
  335. package/dist/pattern.js +239 -0
  336. package/dist/pattern.js.map +1 -0
  337. package/dist/peer.d.ts +17 -0
  338. package/dist/peer.d.ts.map +1 -0
  339. package/dist/peer.js +50 -0
  340. package/dist/peer.js.map +1 -0
  341. package/dist/plugin.d.ts +67 -16
  342. package/dist/plugin.d.ts.map +1 -1
  343. package/dist/plugin.js +649 -0
  344. package/dist/plugin.js.map +1 -0
  345. package/dist/powered-by.d.ts.map +1 -1
  346. package/dist/powered-by.js +11 -0
  347. package/dist/powered-by.js.map +1 -0
  348. package/dist/pretty-json.d.ts.map +1 -1
  349. package/dist/pretty-json.js +47 -0
  350. package/dist/pretty-json.js.map +1 -0
  351. package/dist/producer-stream.d.ts +47 -0
  352. package/dist/producer-stream.d.ts.map +1 -0
  353. package/dist/producer-stream.js +267 -0
  354. package/dist/producer-stream.js.map +1 -0
  355. package/dist/proxy.d.ts +47 -8
  356. package/dist/proxy.d.ts.map +1 -1
  357. package/dist/proxy.js +274 -0
  358. package/dist/proxy.js.map +1 -0
  359. package/dist/python-type-emitter.d.ts +14 -1
  360. package/dist/python-type-emitter.d.ts.map +1 -1
  361. package/dist/python-type-emitter.js +157 -0
  362. package/dist/python-type-emitter.js.map +1 -0
  363. package/dist/realtime/bus.d.ts +17 -1
  364. package/dist/realtime/bus.d.ts.map +1 -1
  365. package/dist/realtime/bus.js +112 -0
  366. package/dist/realtime/bus.js.map +1 -0
  367. package/dist/realtime/register.d.ts +3 -0
  368. package/dist/realtime/register.d.ts.map +1 -0
  369. package/dist/realtime/register.js +10 -0
  370. package/dist/realtime/register.js.map +1 -0
  371. package/dist/realtime/route.d.ts +65 -26
  372. package/dist/realtime/route.d.ts.map +1 -1
  373. package/dist/realtime/route.js +72 -0
  374. package/dist/realtime/route.js.map +1 -0
  375. package/dist/realtime/server.d.ts +43 -0
  376. package/dist/realtime/server.d.ts.map +1 -0
  377. package/dist/realtime/server.js +255 -0
  378. package/dist/realtime/server.js.map +1 -0
  379. package/dist/request-id.d.ts +7 -0
  380. package/dist/request-id.d.ts.map +1 -1
  381. package/dist/request-id.js +16 -0
  382. package/dist/request-id.js.map +1 -0
  383. package/dist/request-limits.d.ts +3 -0
  384. package/dist/request-limits.d.ts.map +1 -0
  385. package/dist/request-limits.js +3 -0
  386. package/dist/request-limits.js.map +1 -0
  387. package/dist/request-path.d.ts +33 -0
  388. package/dist/request-path.d.ts.map +1 -0
  389. package/dist/request-path.js +178 -0
  390. package/dist/request-path.js.map +1 -0
  391. package/dist/request-to-curl.d.ts +7 -1
  392. package/dist/request-to-curl.d.ts.map +1 -1
  393. package/dist/request-to-curl.js +48 -0
  394. package/dist/request-to-curl.js.map +1 -0
  395. package/dist/response.d.ts +25 -2
  396. package/dist/response.d.ts.map +1 -1
  397. package/dist/response.js +313 -0
  398. package/dist/response.js.map +1 -0
  399. package/dist/rust-type-emitter.d.ts +14 -2
  400. package/dist/rust-type-emitter.d.ts.map +1 -1
  401. package/dist/rust-type-emitter.js +463 -0
  402. package/dist/rust-type-emitter.js.map +1 -0
  403. package/dist/secure-headers.d.ts.map +1 -1
  404. package/dist/secure-headers.js +53 -0
  405. package/dist/secure-headers.js.map +1 -0
  406. package/dist/serve-register.d.ts +4 -0
  407. package/dist/serve-register.d.ts.map +1 -1
  408. package/dist/serve-register.js +9 -0
  409. package/dist/serve-register.js.map +1 -0
  410. package/dist/serve-slot.d.ts +3 -0
  411. package/dist/serve-slot.d.ts.map +1 -1
  412. package/dist/serve-slot.js +52 -0
  413. package/dist/serve-slot.js.map +1 -0
  414. package/dist/serve.d.ts +18 -1
  415. package/dist/serve.d.ts.map +1 -1
  416. package/dist/serve.js +157 -0
  417. package/dist/serve.js.map +1 -0
  418. package/dist/server-timing.d.ts.map +1 -1
  419. package/dist/server-timing.js +51 -0
  420. package/dist/server-timing.js.map +1 -0
  421. package/dist/static.d.ts +9 -0
  422. package/dist/static.d.ts.map +1 -1
  423. package/dist/static.js +102 -0
  424. package/dist/static.js.map +1 -0
  425. package/dist/telemetry/otel.d.ts +2 -0
  426. package/dist/telemetry/otel.d.ts.map +1 -1
  427. package/dist/telemetry/otel.js +135 -0
  428. package/dist/telemetry/otel.js.map +1 -0
  429. package/dist/testing.d.ts +12 -0
  430. package/dist/testing.d.ts.map +1 -1
  431. package/dist/testing.js +124 -0
  432. package/dist/testing.js.map +1 -0
  433. package/dist/timeout.d.ts +6 -0
  434. package/dist/timeout.d.ts.map +1 -1
  435. package/dist/timeout.js +36 -0
  436. package/dist/timeout.js.map +1 -0
  437. package/dist/tree.d.ts +133 -36
  438. package/dist/tree.d.ts.map +1 -1
  439. package/dist/tree.js +437 -0
  440. package/dist/tree.js.map +1 -0
  441. package/dist/trust.d.ts +21 -0
  442. package/dist/trust.d.ts.map +1 -0
  443. package/dist/trust.js +100 -0
  444. package/dist/trust.js.map +1 -0
  445. package/dist/ts-type-emitter.d.ts.map +1 -1
  446. package/dist/ts-type-emitter.js +150 -0
  447. package/dist/ts-type-emitter.js.map +1 -0
  448. package/dist/type-emitter.d.ts +16 -2
  449. package/dist/type-emitter.d.ts.map +1 -1
  450. package/dist/type-emitter.js +729 -0
  451. package/dist/type-emitter.js.map +1 -0
  452. package/dist/type-extractor.d.ts +8 -4
  453. package/dist/type-extractor.d.ts.map +1 -1
  454. package/dist/type-extractor.js +819 -0
  455. package/dist/type-extractor.js.map +1 -0
  456. package/dist/types.d.ts +7 -0
  457. package/dist/types.d.ts.map +1 -1
  458. package/dist/types.js +96 -0
  459. package/dist/types.js.map +1 -0
  460. package/dist/validation.d.ts +14 -2
  461. package/dist/validation.d.ts.map +1 -1
  462. package/dist/validation.js +335 -0
  463. package/dist/validation.js.map +1 -0
  464. package/dist/with-headers.d.ts +15 -0
  465. package/dist/with-headers.d.ts.map +1 -0
  466. package/dist/with-headers.js +45 -0
  467. package/dist/with-headers.js.map +1 -0
  468. package/dist/ws/bun.d.ts +11 -5
  469. package/dist/ws/bun.d.ts.map +1 -1
  470. package/dist/ws/bun.js +82 -0
  471. package/dist/ws/bun.js.map +1 -0
  472. package/dist/ws/cloudflare.d.ts +25 -3
  473. package/dist/ws/cloudflare.d.ts.map +1 -1
  474. package/dist/ws/cloudflare.js +117 -0
  475. package/dist/ws/cloudflare.js.map +1 -0
  476. package/dist/ws/deno.d.ts +4 -4
  477. package/dist/ws/deno.d.ts.map +1 -1
  478. package/dist/ws/deno.js +133 -0
  479. package/dist/ws/deno.js.map +1 -0
  480. package/dist/ws/node.d.ts +20 -3
  481. package/dist/ws/node.d.ts.map +1 -1
  482. package/dist/ws/node.js +173 -0
  483. package/dist/ws/node.js.map +1 -0
  484. package/dist/ws/session.d.ts +11 -0
  485. package/dist/ws/session.d.ts.map +1 -0
  486. package/dist/ws/session.js +60 -0
  487. package/dist/ws/session.js.map +1 -0
  488. package/dist/ws/shared.d.ts +65 -0
  489. package/dist/ws/shared.d.ts.map +1 -0
  490. package/dist/ws/shared.js +103 -0
  491. package/dist/ws/shared.js.map +1 -0
  492. package/dist/ws-origin.d.ts +20 -0
  493. package/dist/ws-origin.d.ts.map +1 -0
  494. package/dist/ws-origin.js +41 -0
  495. package/dist/ws-origin.js.map +1 -0
  496. package/dist/yaml.d.ts +15 -1
  497. package/dist/yaml.d.ts.map +1 -1
  498. package/dist/yaml.js +101 -0
  499. package/dist/yaml.js.map +1 -0
  500. package/docs/invalidate-spec.md +13 -1
  501. package/docs/meta-spec.md +36 -12
  502. package/docs/packaging.md +43 -24
  503. package/docs/regression-matrix/README.md +32 -0
  504. package/docs/regression-matrix/review.md +44 -0
  505. package/docs/regression-matrix/ws1-3.md +97 -0
  506. package/docs/regression-matrix/ws4-8.md +107 -0
  507. package/docs/regression-matrix/ws9-12.md +266 -0
  508. package/docs/sdk.md +102 -52
  509. package/package.json +183 -52
  510. package/src/accepts.ts +91 -33
  511. package/src/body-kind.ts +67 -0
  512. package/src/body-limit.ts +102 -87
  513. package/src/build/index.ts +104 -54
  514. package/src/cli-args.ts +123 -0
  515. package/src/cli-go/auth.go +12 -1
  516. package/src/cli-go/config.go +29 -2
  517. package/src/cli-go/errors.go +28 -10
  518. package/src/cli-go/multipart.go +51 -27
  519. package/src/cli-go/output.go +58 -4
  520. package/src/cli-go/root.go +1 -1
  521. package/src/cli-go/version.go +2 -2
  522. package/src/cli.ts +154 -148
  523. package/src/client/error.ts +56 -1
  524. package/src/client/http.ts +479 -256
  525. package/src/client/index.ts +55 -17
  526. package/src/client/path.ts +169 -0
  527. package/src/client/redirect-policy.ts +19 -0
  528. package/src/client/sdk.ts +180 -101
  529. package/src/client/sse.ts +92 -66
  530. package/src/client/ws.ts +67 -31
  531. package/src/client-go/errors.go +41 -12
  532. package/src/client-go/export.go +44 -0
  533. package/src/client-go/invalidation.go +52 -54
  534. package/src/client-go/nullable.go +62 -0
  535. package/src/client-go/realtime.go +147 -110
  536. package/src/client-go/runtime.go +646 -88
  537. package/src/client-go/sse.go +104 -94
  538. package/src/client-go/transport.go +67 -27
  539. package/src/client-go/ws.go +49 -25
  540. package/src/client-info.ts +79 -0
  541. package/src/client-mcp/server.ts +133 -3
  542. package/src/client-python/_invalidation.py +207 -291
  543. package/src/client-python/_realtime.py +63 -40
  544. package/src/client-python/_runtime.py +501 -119
  545. package/src/client-python/_sse.py +130 -74
  546. package/src/client-python/_transport.py +37 -9
  547. package/src/client-python/_ws.py +50 -22
  548. package/src/client-rust/errors.rs +21 -1
  549. package/src/client-rust/invalidation.rs +202 -231
  550. package/src/client-rust/invalidation_sync.rs +16 -229
  551. package/src/client-rust/nullable.rs +17 -0
  552. package/src/client-rust/realtime.rs +217 -217
  553. package/src/client-rust/runtime.rs +602 -177
  554. package/src/client-rust/runtime_sync.rs +169 -145
  555. package/src/client-rust/sse.rs +142 -121
  556. package/src/client-rust/ws.rs +61 -37
  557. package/src/codegen-go-cli.ts +607 -728
  558. package/src/codegen-go.ts +710 -770
  559. package/src/codegen-ir.ts +381 -75
  560. package/src/codegen-lang.ts +517 -0
  561. package/src/codegen-load.ts +150 -0
  562. package/src/codegen-mcp.ts +207 -108
  563. package/src/codegen-python.ts +684 -816
  564. package/src/codegen-route-tree-intern.ts +51 -25
  565. package/src/codegen-rust.ts +946 -1240
  566. package/src/codegen-sanitize.ts +0 -4
  567. package/src/codegen-schema-naming.ts +19 -6
  568. package/src/codegen-sdk-model.ts +228 -0
  569. package/src/codegen.ts +1488 -1140
  570. package/src/context.ts +130 -11
  571. package/src/cookie-sign.ts +85 -16
  572. package/src/cookie.ts +66 -11
  573. package/src/cors.ts +55 -54
  574. package/src/crypto.ts +27 -5
  575. package/src/csrf.ts +57 -25
  576. package/src/curl-logger.ts +155 -114
  577. package/src/dict.ts +12 -0
  578. package/src/error.ts +4 -2
  579. package/src/etag.ts +58 -34
  580. package/src/feature-detect.ts +147 -0
  581. package/src/feature-slots.ts +70 -0
  582. package/src/fetch-request.ts +13 -0
  583. package/src/gen-process.ts +42 -0
  584. package/src/gen-write.ts +127 -0
  585. package/src/glob.ts +109 -0
  586. package/src/go-type-emitter.ts +406 -155
  587. package/src/honey-response.ts +36 -9
  588. package/src/i18n-slot.ts +10 -0
  589. package/src/i18n.ts +98 -23
  590. package/src/index.ts +1887 -1756
  591. package/src/init.ts +40 -28
  592. package/src/invalidate-check.ts +42 -4
  593. package/src/invoke-user.ts +95 -0
  594. package/src/ip-restrict.ts +47 -161
  595. package/src/ip.ts +194 -0
  596. package/src/logger.ts +65 -36
  597. package/src/media-type.ts +104 -0
  598. package/src/meta-spec-merge.ts +80 -0
  599. package/src/meta-spec.ts +0 -0
  600. package/src/middleware.ts +112 -12
  601. package/src/node-request.ts +242 -52
  602. package/src/node.ts +318 -75
  603. package/src/openapi/collect.ts +19 -44
  604. package/src/openapi/docs-page.ts +89 -0
  605. package/src/openapi/document.ts +503 -224
  606. package/src/openapi/json-schema-slot.ts +40 -0
  607. package/src/openapi/register.ts +7 -2
  608. package/src/openapi/scalar.ts +11 -10
  609. package/src/openapi/serve.ts +39 -0
  610. package/src/openapi/spec-factory.ts +50 -1
  611. package/src/openapi/spec.ts +53 -30
  612. package/src/openapi/swagger.ts +12 -11
  613. package/src/pattern.ts +245 -0
  614. package/src/peer.ts +53 -0
  615. package/src/plugin.ts +270 -185
  616. package/src/powered-by.ts +2 -2
  617. package/src/pretty-json.ts +25 -9
  618. package/src/producer-stream.ts +296 -0
  619. package/src/proxy.ts +253 -55
  620. package/src/python-type-emitter.ts +68 -56
  621. package/src/realtime/bus.ts +56 -9
  622. package/src/realtime/register.ts +13 -0
  623. package/src/realtime/route.ts +129 -80
  624. package/src/realtime/server.ts +298 -0
  625. package/src/request-id.ts +13 -3
  626. package/src/request-limits.ts +2 -0
  627. package/src/request-path.ts +165 -0
  628. package/src/request-to-curl.ts +28 -7
  629. package/src/response.ts +143 -103
  630. package/src/rust-type-emitter.ts +265 -170
  631. package/src/secure-headers.ts +8 -4
  632. package/src/serve-register.ts +4 -0
  633. package/src/serve-slot.ts +38 -0
  634. package/src/serve.ts +113 -25
  635. package/src/server-timing.ts +11 -7
  636. package/src/static.ts +85 -32
  637. package/src/telemetry/otel.ts +29 -7
  638. package/src/testing.ts +76 -14
  639. package/src/timeout.ts +15 -7
  640. package/src/tree.ts +405 -333
  641. package/src/trust.ts +117 -0
  642. package/src/ts-type-emitter.ts +18 -12
  643. package/src/type-emitter.ts +227 -89
  644. package/src/type-extractor.ts +603 -423
  645. package/src/types.ts +8 -0
  646. package/src/validation.ts +221 -96
  647. package/src/with-headers.ts +45 -0
  648. package/src/ws/bun.ts +53 -15
  649. package/src/ws/cloudflare.ts +81 -19
  650. package/src/ws/deno.ts +124 -36
  651. package/src/ws/node.ts +173 -79
  652. package/src/ws/session.ts +73 -0
  653. package/src/ws/shared.ts +140 -0
  654. package/src/ws-origin.ts +53 -0
  655. package/src/yaml.ts +45 -22
  656. package/dist/client-mcp/server.d.ts +0 -10
  657. package/dist/client-mcp/server.d.ts.map +0 -1
  658. package/dist/client-mcp/types.d.ts +0 -16
  659. package/dist/client-mcp/types.d.ts.map +0 -1
  660. package/dist/cloudflare/realtime.d.ts +0 -30
  661. package/dist/cloudflare/realtime.d.ts.map +0 -1
  662. package/dist/realtime/buffer.d.ts +0 -18
  663. package/dist/realtime/buffer.d.ts.map +0 -1
  664. package/dist/realtime/longpoll.d.ts +0 -23
  665. package/dist/realtime/longpoll.d.ts.map +0 -1
  666. package/dist/realtime/protocol.d.ts +0 -50
  667. package/dist/realtime/protocol.d.ts.map +0 -1
  668. package/dist/realtime/runtime.d.ts +0 -74
  669. package/dist/realtime/runtime.d.ts.map +0 -1
  670. package/src/cloudflare/realtime.ts +0 -234
  671. package/src/realtime/buffer.ts +0 -101
  672. package/src/realtime/longpoll.ts +0 -148
  673. package/src/realtime/protocol.ts +0 -128
  674. package/src/realtime/runtime.ts +0 -427
package/README.md CHANGED
@@ -90,6 +90,8 @@ curl http://127.0.0.1:3000/openapi.json
90
90
 
91
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
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
+
93
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)`.
94
96
 
95
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.
@@ -113,7 +115,7 @@ Honey is not Express with types bolted on. There is no `req.body` parser stack.
113
115
 
114
116
  ## Install
115
117
 
116
- Requires [Bun](https://bun.sh) 1.3+ to develop this repo. Consumers can run the published package on Bun, Node, Deno, or Workers.
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.
117
119
 
118
120
  ```bash
119
121
  bun add @lovrozagar/honey
@@ -130,7 +132,10 @@ honey init
130
132
  honey generate
131
133
  ```
132
134
 
133
- Peer-free runtime: the published package depends only on `jiti` for generate/watch. Validation libraries are your choice. Type generation (`codegen.types`) needs `ts-morph` as a **dev** dependency.
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`).
134
139
 
135
140
  ## First app
136
141
 
@@ -184,19 +189,21 @@ await app.serve({ env: { DATABASE_URL: process.env.DATABASE_URL! }, port: 3000 }
184
189
  ## CLI
185
190
 
186
191
  ```
187
- honey generate [--watch] [--config <path>] [--app <path>] [flags]
192
+ honey generate [--watch] [--config <path>] [--plugin <n>] [--app <path>] [flags]
188
193
  honey init [--cf] [--force]
189
194
  ```
190
195
 
191
- Any other first argument prints usage and exits `1`.
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.
192
197
 
193
198
  ### `honey init`
194
199
 
195
200
  | Flag | Meaning |
196
201
  | --------- | ----------------------------------------------------------------------- |
197
- | `--cf` | Also write a Workers entry and `wrangler.jsonc` |
202
+ | `--cf` | Also write a Workers entry and `wrangler.jsonc` (alias `--cloudflare`) |
198
203
  | `--force` | Overwrite `src/app.ts`, `src/server.ts`, `vite.config.ts` if they exist |
199
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
+
200
207
  ### `honey generate`
201
208
 
202
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.
@@ -207,6 +214,7 @@ If there is no Vite config, you must pass `--app`.
207
214
  | ------------------------------ | ------------------------------------------------------------------------------------------------------------- |
208
215
  | `--watch` | Regenerate when the route tree checksum changes. Ignores `_gen` / `.gen.*`. Requires `--app` or plugin `app`. |
209
216
  | `--config <path>` | Vite config to load (default `vite.config.ts`) |
217
+ | `--plugin <n>` | Which `honey()` plugin in the config, 0-based, when it holds several |
210
218
  | `--app <path>` | App module, overrides plugin `app` |
211
219
  | `--tree` | Enable writing the generated route tree |
212
220
  | `--types` | Enable writing TypeScript route types (needs `ts-morph`) |
@@ -267,6 +275,8 @@ export default {
267
275
  }
268
276
  ```
269
277
 
278
+ `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.
279
+
270
280
  `codegen.invalidate` (default `"warn"`) lists mutations that declare no `invalidate` but have
271
281
  sibling read routes they plausibly affect. `"error"` to fail the build; `"off"` to silence it.
272
282
 
@@ -295,7 +305,7 @@ app.delete("/items/:id")
295
305
  app.head("/items/:id")
296
306
  app.options("/items")
297
307
  app.all("/echo") // every method
298
- app.on("GET", "/health")
308
+ app.on(["GET"], "/health") // methods are always a list
299
309
  app.on(["GET", "HEAD"], "/resource") // same handler, extra methods
300
310
  ```
301
311
 
@@ -318,10 +328,26 @@ app.get("/files/*path").handler((ctx) => {
318
328
  })
319
329
  ```
320
330
 
321
- - `:name` is one segment.
322
- - `*name` is the remainder of the path.
331
+ - `:name` is one whole segment. Names use letters, digits, `_`, `$` and `-`; `/f/:name.json` is rejected at registration.
332
+ - `:name?` is an optional last segment: `/items/:id?` serves `/items` and `/items/42`.
333
+ - `*name` (or bare `*`, read as `ctx.params["*"]`) is the remainder of the path and must be the last segment.
334
+ - 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.
335
+ - 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.
323
336
  - Static paths (`/health`) use an O(1) map. Dynamic and wildcard segments walk the radix tree.
324
337
 
338
+ #### Request paths
339
+
340
+ 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:
341
+
342
+ - Empty segments collapse: `//admin///users` is `/admin/users`.
343
+ - Dot segments resolve: `/files/../admin` is `/admin`, never above the root; `%2e` counts as a dot.
344
+ - 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.
345
+ - 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.
346
+ - Everything else stays percent-encoded in `ctx.path`; params are decoded when extracted.
347
+ - A trailing slash is kept; `trailingSlash()` decides what it means.
348
+
349
+ 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.
350
+
325
351
  Optional extra validation of params (beyond “it is a string”):
326
352
 
327
353
  ```ts
@@ -334,43 +360,67 @@ app
334
360
  ### Prefixes and slashes
335
361
 
336
362
  ```ts
337
- app.basePath("/api") // every later route is prefixed
338
- app.trailingSlash("strip") // 308 /health/ → /health
363
+ const api = app.basePath("/api") // routes registered on `api` are prefixed
364
+ app.trailingSlash("strip") // 308 /health/ → /health (Location is relative: /health)
339
365
  app.trailingSlash("enforce") // 308 /health → /health/
340
366
  app.trailingSlash("ignore") // both match (default)
341
367
  app.stripPrefix("/app") // inbound /app/api/x is matched as /api/x
342
368
  ```
343
369
 
344
- - `basePath` only affects routes registered **after** the call, on that chain.
370
+ - `basePath` returns a new handle; only routes registered through it (or handles derived from it) are prefixed.
345
371
  - `stripPrefix` is a gateway rewrite. Requests without the prefix still match. It will not strip a partial segment (`/apple` is not stripped by `/app`).
346
- - `.use()` / `.basePath()` / `.context()` / `.meta()` clone the builder and share the route graph with the parent.
372
+ - `.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()`.
373
+ - **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.
374
+ - 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.
347
375
 
348
376
  ### Context
349
377
 
350
378
  `ctx` is a `HoneyContext`. Fields:
351
379
 
352
- | Field | Meaning |
353
- | ------------------------------ | ------------------------------------------------------- |
354
- | `ctx.req` | Web `Request` (on Node serve, a Request-shaped wrapper) |
355
- | `ctx.res` | `HoneyRes` — see [Responses](#responses) |
356
- | `ctx.env` | Bindings you passed to `fetch` / `serve` |
357
- | `ctx.params` | Path params (`:id`, `*path`) |
358
- | `ctx.search` / `ctx.searchAll` | First value / all values of the query string (lazy) |
359
- | `ctx.headers` / `ctx.cookies` | Lazy records (lowercase cookie names as sent) |
360
- | `ctx.input` | Validated input when `.input()` is declared |
361
- | `ctx.errors` | Typed error factory when `.errorFactory()` is set |
362
- | `ctx.meta` | Merged route + chain `.meta()` |
363
- | `ctx.path` | Request pathname after `stripPrefix` |
364
- | `ctx.routePattern` | Registered pattern, e.g. `/users/:id` |
365
- | `ctx.realtime` | `{ publish(topic, data) }` when realtime routes exist |
366
- | `ctx.tap(key, payload)` | Queue a tap (only if `.taps()` was declared) |
367
- | `ctx.background(promise)` | `waitUntil` on Workers, otherwise fire-and-forget |
368
- | `ctx.executionCtx` | Workers `ExecutionContext` when `fetch` received one |
369
- | `ctx.log` | Present when `logger({ instance })` middleware ran |
370
- | `ctx.requestId` | Present when `requestId()` middleware ran |
371
- | `ctx.timing` | Present when `serverTiming()` middleware ran |
372
-
373
- Reserved keys that middleware / `.context()` **cannot** overwrite: `req`, `res`, `env`, `params`, `headers`, `cookies`, `search`, `background`.
380
+ | Field | Meaning |
381
+ | ------------------------------ | ------------------------------------------------------------------------------------------ |
382
+ | `ctx.req` | Web `Request`; on Node serve, a Request-shaped view (see below) |
383
+ | `ctx.res` | `HoneyRes` — see [Responses](#responses) |
384
+ | `ctx.env` | Bindings you passed to `fetch` / `serve` |
385
+ | `ctx.params` | Path params (`:id`, `*path`) |
386
+ | `ctx.search` / `ctx.searchAll` | First value / all values of the query string (lazy) |
387
+ | `ctx.headers` / `ctx.cookies` | Lazy records (lowercase cookie names as sent) |
388
+ | `ctx.ip` | Client IP, canonical; see [Client address](#client-address) |
389
+ | `ctx.input` | Validated input when `.input()` is declared |
390
+ | `ctx.errors` | Typed error factory when `.errorFactory()` is set |
391
+ | `ctx.meta` | Merged route + chain `.meta()` |
392
+ | `ctx.path` | Normalized request path after `stripPrefix` |
393
+ | `ctx.signal` | Aborts on disconnect, `timeout()` or shutdown; see [SSE and streaming](#sse-and-streaming) |
394
+ | `ctx.routePattern` | Registered pattern, e.g. `/users/:id` |
395
+ | `ctx.realtime` | `{ publish, namespace(name).publish }` when realtime routes exist |
396
+ | `ctx.tap(key, payload)` | Queue a tap (only if `.taps()` was declared) |
397
+ | `ctx.background(promise)` | `waitUntil` on Workers, otherwise fire-and-forget |
398
+ | `ctx.executionCtx` | Workers `ExecutionContext` when `fetch` received one |
399
+ | `ctx.log` | Present when `logger({ instance })` middleware ran |
400
+ | `ctx.requestId` | Present when `requestId()` middleware ran |
401
+ | `ctx.timing` | Present when `serverTiming()` middleware ran |
402
+
403
+ 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`, …).
404
+
405
+ `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.
406
+
407
+ 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.
408
+
409
+ #### Client address
410
+
411
+ `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:
412
+
413
+ ```ts
414
+ import "@lovrozagar/honey/trust" // needed for a hop count or a list; not for the default
415
+
416
+ app.trustProxy(false) // default: the TCP peer is the client; X-Forwarded-* is ignored
417
+ app.trustProxy(1) // one reverse proxy in front, appending to X-Forwarded-For (nginx, most load balancers)
418
+ app.trustProxy(["10.0.0.0/8", "fd00::/8"]) // proxies recognized by address; the first other hop is the client
419
+ ```
420
+
421
+ - 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" })`.
422
+ - 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.
423
+ - 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.
374
424
 
375
425
  ```ts
376
426
  app
@@ -516,12 +566,32 @@ app
516
566
  })
517
567
  ```
518
568
 
519
- Content-Type selection:
569
+ Content-Type selection (case-insensitive, parameters ignored):
520
570
 
521
- - `application/json` → `json`
571
+ - `application/json` or any `+json` type (`application/vnd.api+json`) → `json`
522
572
  - `application/x-www-form-urlencoded` or `multipart/form-data` → `form` (`req.formData()`)
573
+ - 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`.
574
+ - 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`.
523
575
  - Invalid input is **400** with `error_key: "validation_failed"` and `fields: { name: [{ error_key, message, path }] }`.
524
576
 
577
+ 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`.
578
+
579
+ Repeated keys in `search` and `form` follow one policy, read from the schema's JSON Schema (Zod 4.2+, ArkType):
580
+
581
+ - 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))`.
582
+ - A key typed as a scalar gets the **first** value, the same one `ctx.search.key` returns.
583
+ - With schemas that expose no JSON Schema (Valibot, Yup, Effect), one value is a scalar and a repeated key is an array.
584
+
585
+ ```ts
586
+ // ?tag=a&tag=b&page=1&page=2
587
+ app
588
+ .get("/items")
589
+ .input({ search: z.object({ tag: z.array(z.string()), page: z.coerce.number() }) })
590
+ .handler((ctx) => ctx.res.json("ok", ctx.input.search)) // { tag: ["a", "b"], page: 1 }
591
+ ```
592
+
593
+ Cookies with a repeated name keep the first; headers follow Fetch (repeated values joined with `, `).
594
+
525
595
  Valibot / ArkType work the same way — pass any Standard Schema:
526
596
 
527
597
  ```ts
@@ -578,6 +648,8 @@ app.outputValidation("always")
578
648
 
579
649
  ### Errors
580
650
 
651
+ <!-- snippet:name errors-app -->
652
+
581
653
  ```ts
582
654
  import { defineErrors, HoneyError, honey } from "@lovrozagar/honey"
583
655
 
@@ -589,6 +661,7 @@ const errors = defineErrors({
589
661
  schema: z.object({ reason: z.string() }),
590
662
  },
591
663
  api_error: "internal_server_error",
664
+ orgs_unavailable: "service_unavailable",
592
665
  })
593
666
 
594
667
  const app = honey()
@@ -640,11 +713,13 @@ Custom-schema errors serialize **the schema payload** as the body (not the envel
640
713
 
641
714
  Per-route:
642
715
 
716
+ <!-- snippet:continue errors-app -->
717
+
643
718
  ```ts
644
719
  app
645
720
  .post("/orgs")
646
- .errors("org_slug_taken", "unauthorized")
647
- .boundary("api_error") // this route's unexpected-throw key
721
+ .errors("org_slug_taken") // default errors ("unauthorized") need no listing
722
+ .boundary("orgs_unavailable") // this route's unexpected-throw key
648
723
  .handler((ctx) => {
649
724
  throw ctx.errors.org_slug_taken({ vars: { slug: "x" } })
650
725
  })
@@ -692,41 +767,63 @@ Messages are ICU: `{slug}`, `{n, number}`, `{n, plural, one {# item} other {# it
692
767
 
693
768
  `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).
694
769
 
695
- Framework-owned keys you do not declare: `validation_failed` (400), `output_validation_failed` (500), plus routing / content-negotiation keys.
770
+ 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`, …).
696
771
 
697
772
  ### Middleware
698
773
 
699
774
  ```ts
700
- import { createMiddleware } from "@lovrozagar/honey"
775
+ import { createMiddleware, defineErrors, honey } from "@lovrozagar/honey"
776
+
777
+ const errors = defineErrors({ unauthorized: "unauthorized" })
778
+ const app = honey().errorFactory(errors)
701
779
 
702
- const withAuth = createMiddleware(async (ctx, next) => {
780
+ const withAuth = createMiddleware(async (ctx: { req: Request }, next) => {
703
781
  const token = ctx.req.headers.get("authorization")
704
- if (!token) throw ctx.errors.unauthorized()
782
+ if (!token) throw errors.unauthorized()
705
783
  return next({ user: { id: "u-1" } })
706
784
  })
707
785
 
708
- app.use(withAuth) // global on this chain
709
- app.use("/admin", withAuth) // prefix-scoped; additions only exist under /admin
786
+ const authed = app.use(withAuth) // routes registered on `authed` run withAuth
787
+ authed.get("/me").handler((ctx) => ctx.res.json("ok", ctx.user))
788
+
789
+ app.use("/admin", withAuth) // scoped: every request under /admin, whichever handle registered the route
710
790
  ```
711
791
 
712
- `createMiddleware` infers additions from `next({ ... })`. Later handlers see `ctx.user`. Return `next()` with no argument to add nothing.
792
+ `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`.
713
793
 
714
- `.use()` clones the builder. Keep the returned value (or keep chaining) if later routes should see the middleware.
794
+ `.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.
795
+
796
+ 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.
797
+
798
+ - **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.
799
+ - **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.
800
+
801
+ ```ts
802
+ const metrics = createMiddleware(async (ctx, next) => {
803
+ const res = await next()
804
+ record(res.status, ctx.error?.errorKey)
805
+ return res
806
+ })
807
+ ```
808
+
809
+ - **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.
810
+ - Middleware `errors` and `meta` count for every route the middleware runs on — chain, scoped or route level.
811
+ - **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.
715
812
 
716
813
  ### Shipped middleware
717
814
 
718
- Import each from its path. Do **not** `import { cors } from "@lovrozagar/honey"`.
815
+ 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())`).
719
816
 
720
817
  #### `cors` — `@lovrozagar/honey/cors`
721
818
 
722
819
  ```ts
723
820
  import { cors } from "@lovrozagar/honey/cors"
724
821
 
725
- app.use(cors())
726
- app.use(
822
+ const api = app.use(cors())
823
+ const custom = app.use(
727
824
  cors({
728
825
  origin: "https://app.example.com", // or "*" | string[] | (origin) => boolean
729
- credentials: true, // wildcard origin is echoed (spec-safe)
826
+ credentials: true, // needs an explicit origin (string, list or predicate); with "*" or none, cors() throws
730
827
  methods: ["GET", "POST"],
731
828
  headers: ["authorization", "content-type"],
732
829
  exposeHeaders: ["x-request-id"],
@@ -735,24 +832,35 @@ app.use(
735
832
  )
736
833
  ```
737
834
 
738
- No `Origin` header → middleware is a no-op. Preflight is `OPTIONS` + `access-control-request-method`. `app.serve({ cors: true })` is `cors()` with defaults. `app.serve({ cors: { origin } })` passes the object through.
835
+ 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.
836
+
837
+ - `credentials: true` requires an explicit `origin`: a wildcard would let every site make credentialed requests and read the answers, so construction throws.
838
+ - The opaque origin `null` (sandboxed iframes, `file:`) is never reflected; any page can produce it.
839
+ - 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.
840
+ - 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.
739
841
 
740
842
  #### `csrf` — `@lovrozagar/honey/csrf`
741
843
 
742
844
  ```ts
743
845
  import { csrf } from "@lovrozagar/honey/csrf"
744
846
 
745
- app.use(csrf({ origin: "https://app.example.com" }))
847
+ const api = app.use(csrf({ origin: "https://app.example.com" }))
746
848
  ```
747
849
 
748
- Safe methods (`GET`/`HEAD`/`OPTIONS`) pass. Non-form JSON also passes. Form posts (`urlencoded` / `multipart` / `text/plain`) need `Sec-Fetch-Site: same-origin` or a matching `Origin`. Failure is **403** `forbidden`.
850
+ Every unsafe request is checked, whatever its Content-Type (the algorithm of Go's `http.CrossOriginProtection`):
851
+
852
+ 1. `GET`, `HEAD` and `OPTIONS` pass.
853
+ 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.
854
+ 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.
855
+
856
+ 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).
749
857
 
750
858
  #### `body-limit` — `@lovrozagar/honey/body-limit`
751
859
 
752
860
  ```ts
753
861
  import { bodyLimit } from "@lovrozagar/honey/body-limit"
754
862
 
755
- app.use(
863
+ const api = app.use(
756
864
  bodyLimit({
757
865
  maxSize: 1_048_576,
758
866
  limits: { "application/json": 64_000, "multipart/": 10_485_760 },
@@ -761,7 +869,7 @@ app.use(
761
869
  )
762
870
  ```
763
871
 
764
- Skipped for GET/HEAD/OPTIONS/DELETE. Oversize is **413** `content_too_large`. `limits` keys are matched with `startsWith` against `Content-Type`.
872
+ 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.
765
873
 
766
874
  #### `logger` — `@lovrozagar/honey/logger`
767
875
 
@@ -774,8 +882,8 @@ const log = createLogger({
774
882
  write: (line) => console.log(line),
775
883
  })
776
884
 
777
- app.use(logger())
778
- app.use(
885
+ const api = app.use(logger())
886
+ const custom = app.use(
779
887
  logger({
780
888
  instance: log, // pino-shaped; sets ctx.log
781
889
  skip: (data) => data.path === "/health",
@@ -787,62 +895,64 @@ app.use(
787
895
  )
788
896
  ```
789
897
 
790
- `createLogger` writes one JSON line per call (`level`, `msg`, `time`, plus `base`). `ctx.log.info("hello")` / `ctx.log.info({ k: 1 }, "hello")`.
898
+ `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.
791
899
 
792
900
  #### `curl-logger` — `@lovrozagar/honey/curl-logger`
793
901
 
794
902
  ```ts
795
- import { curlLogger } from "@lovrozagar/honey/curl-logger"
903
+ import { curlLogger, defaultRedactHeader, defaultRedactQueryParam } from "@lovrozagar/honey/curl-logger"
796
904
 
797
- app.use(
905
+ const api = app.use(
798
906
  curlLogger({
799
907
  body: { maxBytes: 2048, allowContentTypes: ["application/json"] },
800
- redactHeader: (name, value) => (name === "authorization" ? "Bearer ***" : value),
801
- redactQueryParam: (name, value) => (name === "token" ? "***" : value),
802
- skip: (data) => data.path === "/health",
908
+ redactHeader: (name, value) => (name === "x-tenant" ? "***" : defaultRedactHeader(name, value)),
909
+ redactQueryParam: (name, value) => defaultRedactQueryParam(name, value),
910
+ skip: (data) => data.path === "/health", // { duration, method, path, requestId, status }
803
911
  }),
804
912
  )
805
913
  ```
806
914
 
915
+ 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.
916
+
807
917
  #### `request-id` — `@lovrozagar/honey/request-id`
808
918
 
809
919
  ```ts
810
920
  import { requestId } from "@lovrozagar/honey/request-id"
811
921
 
812
- app.use(requestId())
813
- app.use(requestId({ header: "x-request-id", generator: () => crypto.randomUUID() }))
922
+ const api = app.use(requestId())
923
+ const custom = app.use(requestId({ header: "x-request-id", generator: () => crypto.randomUUID() }))
814
924
  ```
815
925
 
816
- Adds `ctx.requestId` and echoes the header on the response. Reuses the inbound header when present.
926
+ 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.
817
927
 
818
928
  #### `etag` — `@lovrozagar/honey/etag`
819
929
 
820
930
  ```ts
821
931
  import { etag } from "@lovrozagar/honey/etag"
822
932
 
823
- app.use(etag()) // weak ETag (default)
824
- app.use(etag({ weak: false }))
933
+ const api = app.use(etag()) // weak ETag (default)
934
+ const custom = app.use(etag({ weak: false }))
825
935
  ```
826
936
 
827
- GET/HEAD only. Skips 4xx and streaming bodies. Responds **304** when `If-None-Match` matches.
937
+ 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 `*`).
828
938
 
829
939
  #### `timeout` — `@lovrozagar/honey/timeout`
830
940
 
831
941
  ```ts
832
942
  import { timeout } from "@lovrozagar/honey/timeout"
833
943
 
834
- app.use(timeout({ duration: 5_000 }))
944
+ const api = app.use(timeout({ duration: 5_000 }))
835
945
  ```
836
946
 
837
- Slow handlers reject with **504** `request_timeout`.
947
+ 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.
838
948
 
839
949
  #### `secure-headers` — `@lovrozagar/honey/secure-headers`
840
950
 
841
951
  ```ts
842
952
  import { secureHeaders } from "@lovrozagar/honey/secure-headers"
843
953
 
844
- app.use(secureHeaders())
845
- app.use(
954
+ const api = app.use(secureHeaders())
955
+ const custom = app.use(
846
956
  secureHeaders({
847
957
  contentSecurityPolicy: "default-src 'self'",
848
958
  strictTransportSecurity: "max-age=63072000; includeSubDomains",
@@ -858,15 +968,15 @@ app.use(
858
968
  )
859
969
  ```
860
970
 
861
- Defaults when omitted: `x-content-type-options: nosniff`, `x-frame-options: SAMEORIGIN`, `referrer-policy: strict-origin-when-cross-origin`, `x-xss-protection: 0`.
971
+ 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.
862
972
 
863
973
  #### `server-timing` — `@lovrozagar/honey/server-timing`
864
974
 
865
975
  ```ts
866
976
  import { serverTiming } from "@lovrozagar/honey/server-timing"
867
977
 
868
- app.use(serverTiming())
869
- app.get("/work").handler((ctx) => {
978
+ const api = app.use(serverTiming())
979
+ api.get("/work").handler((ctx) => {
870
980
  ctx.timing.start("db", "query")
871
981
  ctx.timing.end("db")
872
982
  return ctx.res.json("ok", {})
@@ -877,26 +987,31 @@ app.get("/work").handler((ctx) => {
877
987
 
878
988
  ```ts
879
989
  import { ipRestrict } from "@lovrozagar/honey/ip-restrict"
880
-
881
- app.use(
882
- ipRestrict({
883
- allowList: ["127.0.0.1", "10.0.0.0/8"],
884
- denyList: ["192.168.1.50"],
885
- trustProxy: false, // true: X-Forwarded-For / X-Real-IP after CF
886
- getIp: (req) => req.headers.get("cf-connecting-ip"),
887
- }),
888
- )
990
+ import "@lovrozagar/honey/trust"
991
+
992
+ const api = app
993
+ .trustProxy(1) // behind one reverse proxy; see Client address
994
+ .use(
995
+ ipRestrict({
996
+ allowList: ["127.0.0.1", "10.0.0.0/8"],
997
+ denyList: ["192.168.1.50"],
998
+ }),
999
+ )
889
1000
  ```
890
1001
 
891
- Default IP: `cf-connecting-ip` only. Denied / not-allowed is **403**.
1002
+ - The client address is `ctx.ip`, decided by `app.trustProxy()` — see [Client address](#client-address). `getIp: (req) => ...` replaces it with your own source.
1003
+ - 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`.
1004
+ - 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.
1005
+ - 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**.
1006
+ - What it does not cover: it trusts whatever `trustProxy()` says. Behind a proxy you did not declare, every request has the proxy's address.
892
1007
 
893
1008
  #### `powered-by` — `@lovrozagar/honey/powered-by`
894
1009
 
895
1010
  ```ts
896
1011
  import { poweredBy } from "@lovrozagar/honey/powered-by"
897
1012
 
898
- app.use(poweredBy()) // x-powered-by: Honey
899
- app.use(poweredBy({ name: "api" }))
1013
+ const api = app.use(poweredBy()) // x-powered-by: Honey
1014
+ const custom = app.use(poweredBy({ name: "api" }))
900
1015
  ```
901
1016
 
902
1017
  #### `pretty-json` — `@lovrozagar/honey/pretty-json`
@@ -904,11 +1019,26 @@ app.use(poweredBy({ name: "api" }))
904
1019
  ```ts
905
1020
  import { prettyJson } from "@lovrozagar/honey/pretty-json"
906
1021
 
907
- app.use(prettyJson()) // ?pretty=
908
- app.use(prettyJson({ query: "pretty", space: 2 }))
1022
+ const api = app.use(prettyJson()) // ?pretty=
1023
+ const custom = app.use(prettyJson({ query: "pretty", space: 2 }))
909
1024
  ```
910
1025
 
911
- Only rewrites `application/json` when the query string contains the key.
1026
+ 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.
1027
+
1028
+ #### What the middleware does not cover
1029
+
1030
+ | Middleware | Does not cover |
1031
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
1032
+ | `cors` | Requests reaching the handler: it only controls what a browser lets a page read. Pair it with `csrf` for cookie-authenticated writes. |
1033
+ | `csrf` | Safe methods (keep `GET` side-effect free), same-origin attackers (XSS), non-browser clients (they have no ambient credentials to abuse). |
1034
+ | `ipRestrict` | Proxies you did not declare in `trustProxy()`: behind one, every request has the proxy's address. |
1035
+ | `bodyLimit` | Bodies a handler reads before the middleware runs. Node and Bun `serve()` add their own cap (`maxRequestBodySize`). |
1036
+ | `timeout` | Work that ignores `ctx.signal` keeps running after the 504. |
1037
+ | `secureHeaders` | Content it does not see: a header the handler set wins, and the CSP is yours to write. |
1038
+ | `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. |
1039
+ | `proxy()` | Where requests go: build the URL from a fixed origin in `destination`, never from request data. Upstream responses are passed through, not filtered. |
1040
+ | `requestId` | Uniqueness of inbound ids: a client can still send an id that collides with another request's. |
1041
+ | loggers | Secrets in bodies (`curlLogger({ body })`) and in header or param names the default patterns miss; extend the redact callbacks. |
912
1042
 
913
1043
  ### Composition
914
1044
 
@@ -919,9 +1049,10 @@ const users = honey()
919
1049
  .handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
920
1050
 
921
1051
  const app = honey().route(users) // merges routes, realtime, taps, static map
1052
+ const v1 = honey().route("/v1", users) // serves /v1/users/:id
922
1053
  ```
923
1054
 
924
- `.route(sub)` copies the sub-app's tree into this one. Duplicate paths throw.
1055
+ `.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.
925
1056
 
926
1057
  ### Taps
927
1058
 
@@ -947,29 +1078,34 @@ const app = honey()
947
1078
 
948
1079
  ### Proxy routes
949
1080
 
950
- Finish a route with `.proxy()` instead of `.handler()`:
1081
+ 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.
951
1082
 
952
1083
  ```ts
1084
+ import "@lovrozagar/honey/proxy"
1085
+
953
1086
  app.all("/upstream/*path").proxy({
954
1087
  destination: (ctx, url, init) => fetch(`https://api.internal${url}`, init),
955
1088
  rewriteUrl: (url) => url.replace(/^\/upstream/, ""),
956
1089
  requestHeaders: { "x-forwarded-by": "honey" },
957
1090
  // or requestHeaders: (ctx, headers) => { headers.set("x-user", ctx.user.id) }
958
- timeout: 10_000, // or (ctx) => 5_000; omit for no abort; disabled for WS upgrades
1091
+ timeout: 10_000, // time to response headers (default 60 s); 0 for none; disabled for WS upgrades
1092
+ idleTimeout: 30_000, // max gap between body chunks; omit for none (SSE)
959
1093
  onResponse: (ctx, response) => {
960
1094
  response.headers.set("x-proxied", "1")
961
1095
  },
962
1096
  })
963
1097
  ```
964
1098
 
965
- `destination` receives path + query (after `rewriteUrl`) and a prepared `RequestInit` (method, headers, body, signal, redirect). Hop-by-hop headers are stripped. `onResponse` is not called for 101 upgrades.
1099
+ `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.
1100
+
1101
+ What it does not cover: `destination` decides where the request goes — build the URL from a fixed origin (as above), never from request data.
966
1102
 
967
1103
  ### Static files
968
1104
 
969
1105
  ```ts
970
1106
  import { staticFiles } from "@lovrozagar/honey/static"
971
1107
 
972
- app.use(
1108
+ const site = app.use(
973
1109
  staticFiles({
974
1110
  prefix: "/assets",
975
1111
  resolve: async (_ctx, filePath) => {
@@ -984,7 +1120,11 @@ app.use(
984
1120
  )
985
1121
  ```
986
1122
 
987
- GET/HEAD only. `..` segments are rejected. `resolve` returning `null` falls through to the next route.
1123
+ GET/HEAD only. `resolve` returning `null` falls through to the next route.
1124
+
1125
+ - `staticFiles` matches `ctx.path` — the normalized path the router saw, after `stripPrefix` — on whole segments: `prefix: "/assets"` serves `/assets/x`, never `/assets-private/x`.
1126
+ - `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.
1127
+ - `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.
988
1128
 
989
1129
  ### Logging, telemetry, production tree
990
1130
 
@@ -1003,8 +1143,8 @@ app.telemetry(otelAdapter({ tracer }))
1003
1143
  // or a hand-rolled adapter:
1004
1144
  app.telemetry({
1005
1145
  onRequest: ({ req }) => {},
1006
- onRoute: ({ method, path, params }) => {},
1007
- onHandler: ({ status, duration }) => {},
1146
+ onRoute: ({ method, path, route, params }) => {},
1147
+ onHandler: ({ route, status, duration }) => {},
1008
1148
  onResponse: ({ status, duration }) => {},
1009
1149
  onError: ({ error, duration }) => {},
1010
1150
  onMiddleware: ({ name, duration, error }) => {},
@@ -1013,16 +1153,34 @@ app.telemetry({
1013
1153
  })
1014
1154
  ```
1015
1155
 
1016
- Production tree (unknown paths 404 without a catch-all walk):
1156
+ `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).
1157
+
1158
+ 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.
1017
1159
 
1018
1160
  ```ts
1019
1161
  import { routeTree } from "./_gen/routes.gen.ts"
1020
1162
 
1021
- app.routeTree(routeTree)
1022
- const snapshot = app.toRouteTree()
1163
+ const app = honey().routeTree(routeTree)
1164
+ app.get("/health").handler((ctx) => ctx.res.text("ok", "ok"))
1023
1165
  ```
1024
1166
 
1025
- `mergeTree` from `honey` / `honey/tree` merges several trees (optional extra meta per tree).
1167
+ - 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`.
1168
+ - A tree leaf the app does not register is answered with 404 but still documented by `app.openapi()`.
1169
+ - Spec, docs and manifest routes are not part of the generated tree; `app.openapi()` mounts them either way.
1170
+ - `routeTree()` must come before any route registration and can be called once.
1171
+
1172
+ `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.
1173
+
1174
+ 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.
1175
+
1176
+ ```ts
1177
+ import { routeTree } from "./_gen/routes.gen.ts"
1178
+
1179
+ import "@lovrozagar/honey/proxy"
1180
+
1181
+ const gateway = honey().routeTree(routeTree)
1182
+ gateway.all("/*").proxy({ destination: (ctx) => `https://${ctx.meta.worker}.internal${ctx.path}` })
1183
+ ```
1026
1184
 
1027
1185
  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.
1028
1186
 
@@ -1053,11 +1211,25 @@ handle.url // http://127.0.0.1:3000
1053
1211
  handle.port
1054
1212
  handle.hostname
1055
1213
  handle.runtime // "bun" | "node" | "deno"
1056
- await handle.close()
1214
+ await handle.close() // or close(timeoutMs), default 1000
1057
1215
  ```
1058
1216
 
1059
1217
  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`.
1060
1218
 
1219
+ More options:
1220
+
1221
+ | Option | Runtimes | Meaning |
1222
+ | -------------------- | --------- | ---------------------------------------------------------------------------------------- |
1223
+ | `maxRequestBodySize` | Node, Bun | Largest request body in bytes; bigger is 413 and the connection closes. Default 128 MiB. |
1224
+ | `headersTimeout` | Node | `server.headersTimeout` (ms) |
1225
+ | `requestTimeout` | Node | `server.requestTimeout` (ms) |
1226
+ | `keepAliveTimeout` | Node | `server.keepAliveTimeout` (ms) |
1227
+ | `upgradeTimeout` | Node | How long an upgrade request may take to become a WebSocket (ms). Default 30 000. |
1228
+
1229
+ `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).
1230
+
1231
+ `app.serve()` keeps an adapter you set with `app.wsAdapter(...)` (for example `nodeWebSocket({ keepalive })`), and attaches the runtime's default adapter otherwise.
1232
+
1061
1233
  | Runtime | How it listens |
1062
1234
  | ------- | --------------------------------------------- |
1063
1235
  | Bun | `Bun.serve({ fetch })` + `honey/ws/bun` |
@@ -1071,9 +1243,17 @@ Low-level Node listen (same adapter, no runtime detect):
1071
1243
  ```ts
1072
1244
  import { serve } from "@lovrozagar/honey/node"
1073
1245
  const server = serve(app, { env: {}, port: 3000, hostname: "0.0.0.0" })
1074
- await server.shutdown()
1246
+ await server.shutdown(10_000)
1075
1247
  ```
1076
1248
 
1249
+ `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.
1250
+
1251
+ 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).
1252
+
1253
+ 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.
1254
+
1255
+ 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.
1256
+
1077
1257
  `runtime: "cloudflare"` throws. Workers cannot listen.
1078
1258
 
1079
1259
  Detect without serving:
@@ -1124,6 +1304,8 @@ createBuildPlugin(
1124
1304
  )
1125
1305
  ```
1126
1306
 
1307
+ 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 }`.
1308
+
1127
1309
  ## OpenAPI, docs, and manifest
1128
1310
 
1129
1311
  ```ts
@@ -1132,18 +1314,23 @@ app.openapi({
1132
1314
  version: "1.0.0",
1133
1315
  description: "Optional",
1134
1316
  docs: "scalar", // or "swagger"
1135
- docsPath: "/docs", // default /docs, then /reference if /docs is taken
1317
+ docsPath: "/docs", // default /docs, then /reference if a user route owns /docs
1136
1318
  path: "/openapi", // stem → /openapi.json, .yaml, .yml
1137
1319
  filterRoutes: (route) => route.path !== "/debug",
1138
1320
  securitySchemes: {
1139
1321
  bearerAuth: { type: "http", scheme: "bearer" },
1140
1322
  },
1323
+ enabled: process.env.NODE_ENV !== "production", // false mounts nothing; default true
1141
1324
  })
1142
1325
 
1143
1326
  app.manifest()
1144
1327
  app.manifest({ path: "/manifest.json" })
1145
1328
  ```
1146
1329
 
1330
+ 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`.
1331
+
1332
+ 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`.
1333
+
1147
1334
  Served (cached, invalidated when the route graph changes):
1148
1335
 
1149
1336
  | Path | Body |
@@ -1153,12 +1340,14 @@ Served (cached, invalidated when the route graph changes):
1153
1340
  | `/docs` | Scalar or Swagger UI pointing at the JSON spec |
1154
1341
  | `/manifest.json` | Route methods, paths, middleware names, error keys |
1155
1342
 
1156
- Those routes are marked internal. They do not appear inside the spec or the manifest. If `/docs` is already a user route, pass `docsPath` or Honey throws.
1343
+ 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.
1157
1344
 
1158
1345
  `.meta({ operationId, tags, summary, description })` on a route feeds the document. App-level `.meta<Shape>()` / `.meta({ auth: "required" })` sets defaults and constrains route meta.
1159
1346
 
1160
1347
  Generate-time sanitize (plugin `codegen.openApi.sanitize`):
1161
1348
 
1349
+ <!-- snippet:skip -->
1350
+
1162
1351
  ```ts
1163
1352
  {
1164
1353
  stripSecuritySchemes: ["legacy"],
@@ -1212,12 +1401,12 @@ A fact that middleware already enforces should not be retyped on every route it
1212
1401
  ```ts
1213
1402
  export const shard = createMiddleware(fn, { meta: { tenant: "project_id" } })
1214
1403
 
1215
- app.use(shard) // every route on the chain — or app.use("/orgs", shard), or .get("/x").use(shard)
1404
+ const tenant = app.use(shard) // every route registered on `tenant` — or app.use("/orgs", shard), or .get("/x").use(shard)
1216
1405
  ```
1217
1406
 
1218
- Explicit `.meta()` (route, then chain) always outranks a contributed value; among middleware, later mount order wins. 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 back-fills them. `internal` may not be contributed, since a middleware that removed routes from the document would be invisible from both sides.
1407
+ 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.
1219
1408
 
1220
- Resolved once at registration, never per request: the precompiled route tree bakes `mt` as a literal and patch mode does not overwrite it, so a lazily-derived value would differ between dev and production. Change what a middleware contributes, then regenerate.
1409
+ 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.
1221
1410
 
1222
1411
  ## WebSockets
1223
1412
 
@@ -1238,56 +1427,115 @@ app.ws("/echo-ws").handler({
1238
1427
  })
1239
1428
  ```
1240
1429
 
1241
- `ws` is `{ send, close, readyState, raw }`. `readyState` is `0|1|2|3`. Sends before open are buffered (max 32). Middleware on the chain runs for the HTTP upgrade. Auth middleware works: throw to reject.
1430
+ `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.
1431
+
1432
+ 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.
1433
+
1434
+ **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:
1435
+
1436
+ ```ts
1437
+ app
1438
+ .ws("/feed")
1439
+ .origins(["https://app.example.com"]) // or "*", or (origin) => boolean
1440
+ .handler({
1441
+ onOpen(_ctx, ws) {
1442
+ ws.send("hi")
1443
+ },
1444
+ })
1445
+
1446
+ app.realtime("/rt", { allowedOrigins: ["https://app.example.com"], handler })
1447
+ ```
1448
+
1449
+ "Same origin" compares the `Origin` host with the request host (`clientInfo(ctx).host`, which follows `trustProxy`).
1450
+
1451
+ **Adapter options.** Every adapter takes the same options with the same defaults:
1452
+
1453
+ | Option | Default | Meaning |
1454
+ | -------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1455
+ | `maxPayload` | 1 MiB | Largest inbound message; bigger closes with 1009 (Bun drops the connection: 1006). |
1456
+ | `backpressureLimit` | 8 MiB | Unsent outbound bytes before `backpressurePolicy` applies to the next `send()`. |
1457
+ | `backpressurePolicy` | `"close"` | `"close"` closes with 1013 (try again later); `"drop"` discards the message. |
1458
+ | `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. |
1459
+ | `protocol` | first offered | `(offered, req) => string \| null`: the subprotocol to answer with. |
1460
+
1461
+ `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.
1462
+
1463
+ ```ts
1464
+ app.wsAdapter(nodeWebSocket({ maxPayload: 64 * 1024, keepalive: { interval: 30_000, timeout: 10_000 } }))
1465
+ ```
1242
1466
 
1243
1467
  A GET **without** `Upgrade: websocket` to a WS/realtime path returns **426** with `upgrade: websocket`.
1244
1468
 
1245
1469
  Reconnect: pass `?reconnect_token=` on the next handshake; Honey calls `onReconnect` instead of `onOpen`.
1246
1470
 
1247
- `app.serve()` attaches the runtime adapter. On Cloudflare, call `app.wsAdapter(cfWebSocket())` yourself.
1471
+ `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"`.
1248
1472
 
1249
1473
  ## Realtime
1250
1474
 
1251
- Honey's room protocol (not a raw socket). Transports: `ws`, `sse`, `longpoll`.
1475
+ 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.
1252
1476
 
1253
1477
  ```ts
1254
- app.realtime("/realtime/chat/:roomId", {
1255
- reconnectBuffer: 32,
1256
- use: [withAuth], // extra middleware for this socket only
1478
+ import "@lovrozagar/honey/realtime"
1479
+
1480
+ const authed = app.use(withAuth)
1481
+
1482
+ authed.realtime("/realtime/chat/:roomId", {
1483
+ namespace: "chat",
1484
+ identify: (ctx) => ctx.user.id, // becomes conn.userId; a throw rejects the upgrade
1257
1485
  handler: (ctx, conn) => {
1258
- conn.join("room:default")
1486
+ const room = `room:${ctx.params.roomId}`
1487
+ conn.join(room)
1259
1488
  conn.send({ event: "joined", id: conn.id, userId: conn.userId })
1260
1489
  conn.on("message", (payload) => {
1261
- conn.publish("room:default", payload)
1490
+ conn.publish(room, { from: conn.userId, payload })
1262
1491
  })
1263
- conn.on("close", (_reason) => {
1264
- conn.leave("room:default")
1492
+ conn.on("close", () => {
1493
+ conn.publish(room, { left: conn.userId })
1265
1494
  })
1266
1495
  },
1496
+ onError: (error, conn) => log.error({ error, conn: conn.id }),
1267
1497
  })
1268
1498
 
1269
- app.post("/realtime/broadcast/:topic").handler(async (ctx) => {
1270
- const body = await ctx.req.json()
1271
- ctx.realtime.publish(ctx.params.topic, body)
1499
+ // publishing from HTTP needs the same auth as the socket
1500
+ authed.post("/realtime/broadcast/:roomId").handler(async (ctx) => {
1501
+ ctx.realtime.namespace("chat").publish(`room:${ctx.params.roomId}`, await ctx.req.json())
1272
1502
  return ctx.res.json("ok", { published: true })
1273
1503
  })
1274
1504
  ```
1275
1505
 
1506
+ **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.
1507
+
1508
+ **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.
1509
+
1510
+ **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.
1511
+
1276
1512
  `conn`:
1277
1513
 
1278
- | Field | Meaning |
1279
- | ------------------------------ | ----------------------------- |
1280
- | `id` | Connection id |
1281
- | `userId` | `string \| null` |
1282
- | `transport` | `"ws" \| "sse" \| "longpoll"` |
1283
- | `state` | Mutable bag |
1284
- | `join(topic)` / `leave(topic)` | Topic membership |
1285
- | `send(payload)` | To this connection |
1286
- | `publish(topic, payload)` | To everyone on the topic |
1287
- | `close(reason?)` | Disconnect |
1288
- | `on("message" \| "close", fn)` | Inbound / teardown |
1514
+ | Field | Meaning |
1515
+ | ------------------------------ | ------------------------------------------ |
1516
+ | `id` | Connection id |
1517
+ | `userId` | What `identify(ctx)` returned, else `null` |
1518
+ | `transport` | Always `"ws"` |
1519
+ | `closed` | `true` once closed |
1520
+ | `state` | Mutable bag |
1521
+ | `join(topic)` / `leave(topic)` | Topic membership in this route's namespace |
1522
+ | `send(payload)` | One JSON frame to this connection |
1523
+ | `publish(topic, payload)` | One JSON frame to everyone on the topic |
1524
+ | `close(reason?)` | Close with 1000 (reason cut to 123 bytes) |
1525
+ | `on("message" \| "close", fn)` | Inbound frames / teardown |
1289
1526
 
1290
- Duplicate `realtime()` paths throw. Cloudflare's isolate-local bus does **not** cross isolates — REST publish to another connection is skipped on the CF e2e env.
1527
+ `limits` (per connection):
1528
+
1529
+ | Option | Default | Past it |
1530
+ | ------------------ | ------- | ------------------------------------------------------------ |
1531
+ | `maxTopics` | 128 | `join` throws |
1532
+ | `maxFrameBytes` | 1 MiB | close 1009 |
1533
+ | `maxPendingFrames` | 64 | close 1008 (frames waiting for a slow or missing handler) |
1534
+ | `maxBufferedBytes` | 4 MiB | `slowConsumer`: `"close"` (1013, default) or `"drop"` frames |
1535
+
1536
+ `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.
1537
+
1538
+ 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).
1291
1539
 
1292
1540
  ## SSE and streaming
1293
1541
 
@@ -1298,9 +1546,13 @@ app.get("/events").handler((ctx) =>
1298
1546
  if (stream.lastEventId) {
1299
1547
  await stream.send({ event: "resume", data: `from ${stream.lastEventId}` })
1300
1548
  }
1301
- await stream.send({ event: "tick", data: "hello", id: "1", retry: 3000 })
1302
- await stream.send({ event: "data", data: { n: 42 }, id: "2" })
1303
- stream.close()
1549
+ try {
1550
+ for (const update of await feed.since(stream.lastEventId, { signal: stream.signal })) {
1551
+ await stream.send({ event: "data", data: update, id: update.id })
1552
+ }
1553
+ } finally {
1554
+ stream.close()
1555
+ }
1304
1556
  },
1305
1557
  { defaultRetry: 3000, keepalive: 15_000 },
1306
1558
  ),
@@ -1311,24 +1563,40 @@ app.get("/events").handler((ctx) =>
1311
1563
 
1312
1564
  ```ts
1313
1565
  app.get("/stream").handler((ctx) =>
1314
- ctx.res.stream(async (writable) => {
1566
+ ctx.res.stream(async (writable, signal) => {
1315
1567
  const w = writable.getWriter()
1316
- await w.write(new TextEncoder().encode("chunk"))
1317
- await w.close()
1568
+ while (!signal.aborted) {
1569
+ await w.write(new TextEncoder().encode("chunk"))
1570
+ await sleep(1_000)
1571
+ }
1318
1572
  }),
1319
1573
  )
1320
1574
 
1321
1575
  app.get("/gen").handler((ctx) =>
1322
1576
  ctx.res.generate(
1323
1577
  (async function* () {
1324
- yield "one"
1325
- yield "two"
1578
+ try {
1579
+ yield "one"
1580
+ yield "two"
1581
+ } finally {
1582
+ /* runs on a normal end, a disconnect, a timeout or shutdown */
1583
+ }
1326
1584
  })(),
1327
1585
  { contentType: "text/plain", status: 200 },
1328
1586
  ),
1329
1587
  )
1330
1588
  ```
1331
1589
 
1590
+ All three share one lifecycle:
1591
+
1592
+ - **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.
1593
+ - **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.
1594
+ - **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`.
1595
+ - **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.
1596
+ - **Flushing.** `ctx.res.stream()` ends the body after queued writes flush when the callback returns, whether or not you closed the writable.
1597
+
1598
+ `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.
1599
+
1332
1600
  ## Generated clients
1333
1601
 
1334
1602
  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.
@@ -1341,6 +1609,27 @@ See [honey generate](#honey-generate). `codegen.sdk: true` is TypeScript-only in
1341
1609
 
1342
1610
  Hand-typed client against `InferRoutes<typeof app>` (no generate):
1343
1611
 
1612
+ <!-- snippet:define client-app
1613
+ ```ts
1614
+ import { honey } from "@lovrozagar/honey"
1615
+ import * as z from "zod"
1616
+
1617
+ const root = honey()
1618
+ root.ws("/echo-ws").handler({ onMessage: (_ctx, ws, data) => ws.send(data) })
1619
+ const app = root
1620
+ .basePath("/api")
1621
+ .get("/health")
1622
+ .handler((ctx) => ctx.res.json("ok", { status: "ok" }))
1623
+ .get("/users/:id")
1624
+ .handler((ctx) => ctx.res.json("ok", { id: ctx.params.id }))
1625
+ .post("/users")
1626
+ .input({ json: z.object({ email: z.string(), name: z.string() }) })
1627
+ .output({ "application/json": { created: z.object({ id: z.string() }) } })
1628
+ .handler((ctx) => ctx.res.json("created", { id: "u-1" }))
1629
+ ```
1630
+ -->
1631
+ <!-- snippet:continue client-app -->
1632
+
1344
1633
  ```ts
1345
1634
  import { createClient, isClientError } from "@lovrozagar/honey/client"
1346
1635
 
@@ -1378,7 +1667,16 @@ const ws = client.ws("/echo-ws", { reconnectToken: "t" })
1378
1667
 
1379
1668
  Per-call options: `json`, `form`, `search`, `params`, `headers`, `cookies`, `timeout`, `signal`, `lastEventId`.
1380
1669
 
1381
- `throwOnError: true` throws `ClientError` subclasses (`BadRequestError`, `UnauthorizedError`, …). `isClientError(e)` is the guard.
1670
+ More config:
1671
+
1672
+ - `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 "`).
1673
+ - `redirect` — redirects are followed only to the same origin by default, so custom auth headers never follow a cross-origin redirect.
1674
+ - `requestId: false` stops the client from sending `x-request-id` (which makes every cross-origin request preflighted).
1675
+ - A path param that is `""`, `.` or `..` throws `PathParamError` before anything is sent.
1676
+
1677
+ `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.
1678
+
1679
+ 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`).
1382
1680
 
1383
1681
  ### Generated SDK usage
1384
1682
 
@@ -1411,12 +1709,19 @@ const browserSdk = new MySDK({
1411
1709
  const user = await sdk.createUser({ json: { email: "a@b.com", name: "Ada" } })
1412
1710
  ```
1413
1711
 
1712
+ - 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.
1713
+ - 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`).
1714
+ - 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.
1715
+ - 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.
1716
+
1414
1717
  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.
1415
1718
 
1416
1719
  Capability details and four-language snippets: [SDK index](./docs/sdk.md) and [examples](https://github.com/lovrozagar/honey/tree/main/packages/core/examples).
1417
1720
 
1418
1721
  ### Go CLI
1419
1722
 
1723
+ <!-- snippet:skip -->
1724
+
1420
1725
  ```ts
1421
1726
  codegen: {
1422
1727
  cli: {
@@ -1456,26 +1761,28 @@ Language printers used by the plugin (`generatePythonSDK`, `generateGoSDK`, `gen
1456
1761
 
1457
1762
  Exported from `@lovrozagar/honey`. They describe the **app you built**:
1458
1763
 
1459
- | Type | Meaning |
1460
- | ---------------------------------------------------- | ------------------------------------ |
1461
- | `InferRoutes<typeof app>` | Path → methods → input/output/errors |
1462
- | `InferRoutePaths<typeof app>` | Union of paths |
1463
- | `InferRouteMethods<typeof app, Path>` | Methods on one path |
1464
- | `InferRouteInput<typeof app, Path, Method>` | Validated input |
1465
- | `InferRouteOutput<typeof app, Path, Method>` | Output map |
1466
- | `InferRouteErrors<typeof app, Path, Method>` | Declared error keys |
1467
- | `InferRouteMeta<typeof app, Path, Method>` | Route meta |
1468
- | `InferRouteCtx<typeof app, Path, Method>` | Handler ctx for one route |
1469
- | `InferCtx<typeof app>` | Handler context (no `res` brand) |
1470
- | `InferEnv<typeof app>` | `ctx.env` |
1471
- | `InferMeta<typeof app>` | App-level meta |
1472
- | `InferErrorFactory<typeof app>` | Error factory |
1473
- | `InferBasePath<typeof app>` | Base path string |
1474
- | `StatusKey` / `SuccessStatusKey` | Status-key unions |
1475
- | `HoneyCtx<TEnv>` | Untyped context shape |
1476
- | `HoneyServeOptions` / `ServeHandle` / `ServeRuntime` | Serve types |
1477
- | `WSHandler` / `WSContext` / `WSAdapter` | Socket types |
1478
- | `ConnContext` / `RealtimeRouteOpts` | Realtime types |
1764
+ | Type | Meaning |
1765
+ | ---------------------------------------------------------------------------- | ------------------------------------ |
1766
+ | `InferRoutes<typeof app>` | Path → methods → input/output/errors |
1767
+ | `InferRoutePaths<typeof app>` | Union of paths |
1768
+ | `InferRouteMethods<typeof app, Path>` | Methods on one path |
1769
+ | `InferRouteInput<typeof app, Path, Method>` | Validated input |
1770
+ | `InferRouteOutput<typeof app, Path, Method>` | Output map |
1771
+ | `InferRouteErrors<typeof app, Path, Method>` | Declared error keys |
1772
+ | `InferRouteMeta<typeof app, Path, Method>` | Route meta |
1773
+ | `InferRouteCtx<typeof app, Path, Method>` | Handler ctx for one route |
1774
+ | `InferCtx<typeof app>` | Handler context (no `res` brand) |
1775
+ | `InferEnv<typeof app>` | `ctx.env` |
1776
+ | `InferMeta<typeof app>` | App-level meta |
1777
+ | `InferErrorFactory<typeof app>` | Error factory |
1778
+ | `InferBasePath<typeof app>` | Base path string |
1779
+ | `StatusKey` / `SuccessStatusKey` | Status-key unions |
1780
+ | `HoneyCtx<TEnv>` | Untyped context shape |
1781
+ | `HoneyServeOptions` / `ServeHandle` / `ServeRuntime` | Serve types |
1782
+ | `WSHandler` / `WSContext` / `WSAdapter` | Socket types |
1783
+ | `ConnContext` / `RealtimeRouteOpts` / `RealtimeLimits` / `RealtimePublisher` | Realtime types |
1784
+
1785
+ <!-- snippet:continue client-app -->
1479
1786
 
1480
1787
  ```ts
1481
1788
  import type { InferCtx, InferRouteInput, InferRoutes } from "@lovrozagar/honey"
@@ -1501,7 +1808,9 @@ await client.post("/api/users", {
1501
1808
  await client.post("/login", { form: { user: "a", pass: "b" } })
1502
1809
  ```
1503
1810
 
1504
- Methods: `get` `post` `put` `patch` `delete` `head` `options` `request`. `cookies: true` stores `Set-Cookie` and sends them back.
1811
+ 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.
1812
+
1813
+ `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.
1505
1814
 
1506
1815
  ## Utilities
1507
1816
 
@@ -1513,8 +1822,9 @@ import { serializeCookie } from "@lovrozagar/honey/cookie"
1513
1822
  serializeCookie("sid", { value: "abc", httpOnly: true, path: "/" })
1514
1823
 
1515
1824
  import { sign, verify } from "@lovrozagar/honey/cookie-sign"
1516
- const signed = await sign("abc", SECRET)
1517
- const raw = await verify(signed, [SECRET, OLD_SECRET]) // null if none match
1825
+ const signed = await sign("abc", SECRET, { name: "sid" }) // binds the cookie name
1826
+ const raw = await verify(signed, [SECRET, OLD_SECRET], { name: "sid" }) // null if none match or malformed
1827
+ // `legacy: false` stops accepting signatures from earlier releases, which were not bound to a name
1518
1828
 
1519
1829
  import { timingSafeEqual } from "@lovrozagar/honey/crypto"
1520
1830
  await timingSafeEqual(a, b)
@@ -1529,31 +1839,34 @@ await requestToCurl(req, { excludeHeader: (n) => n === "authorization" })
1529
1839
 
1530
1840
  Import features from their path.
1531
1841
 
1532
- | Export | Purpose |
1533
- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
1534
- | `@lovrozagar/honey` | `honey`, `defineErrors`, `HoneyError`, `createMiddleware`, `HoneyRes`, Infer* types, `mergeTree`, `detectRuntime` |
1535
- | `honey/serve` | Register Node/Bun/Deno listen |
1536
- | `honey/node` | Low-level Node `serve()` |
1537
- | `honey/openapi` | Register live spec generation (pulls codegen) |
1538
- | `honey/openapi/spec` | Runtime spec walker — live tree, no codegen. Intern omits JSON Schema |
1539
- | `honey/openapi/scalar` `honey/openapi/swagger` | Docs UI |
1540
- | `honey/i18n` | Register error i18n |
1541
- | `@lovrozagar/honey/plugin` | Vite plugin + `generateFromApp` |
1542
- | `honey/client` / `honey/client/sdk` | Typed TS client runtime / generated-SDK helpers |
1543
- | `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 |
1544
- | `honey/proxy` | `ProxyConfig` type (`.proxy()` lives on the route builder) |
1545
- | `honey/input` | `readableStream()` |
1546
- | `honey/testing` | `testClient` |
1547
- | `honey/ws/bun` `honey/ws/node` `honey/ws/deno` `honey/ws/cloudflare` | WS adapters |
1548
- | `honey/accepts` `honey/cookie` `honey/cookie-sign` `honey/crypto` `honey/request-to-curl` | Utilities |
1549
- | `honey/telemetry/otel` | `otelAdapter` |
1550
- | `honey/codegen` | `generateOpenApi`, `generateSDK`, `generateManifest`, trees, sanitize |
1551
- | `honey/codegen-go-cli` | `generateGoCLI` |
1552
- | `honey/codegen/extract` | ts-morph chain extractor |
1553
- | `honey/build` | `createBuildPlugin` |
1554
- | `honey/tree` | Tree types + `mergeTree` |
1555
- | `honey/cli` | `honey` binary |
1556
- | `honey/errors` | `defineErrors` (also on the root export) |
1842
+ | Export | Purpose |
1843
+ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
1844
+ | `@lovrozagar/honey` | `honey`, `defineErrors`, `HoneyError`, `createMiddleware`, `HoneyRes`, Infer* types, `mergeTree`, `detectRuntime` |
1845
+ | `honey/serve` | Register Node/Bun/Deno listen |
1846
+ | `honey/node` | Low-level Node `serve()` |
1847
+ | `honey/openapi` | Register live spec generation (pulls codegen) |
1848
+ | `honey/openapi/spec` | Runtime spec walker — live tree, no codegen. Intern omits JSON Schema |
1849
+ | `honey/openapi/scalar` `honey/openapi/swagger` | Docs UI |
1850
+ | `honey/i18n` | Register error i18n |
1851
+ | `@lovrozagar/honey/plugin` | Vite plugin + `generateFromApp` |
1852
+ | `honey/client` / `honey/client/sdk` | Typed TS client runtime / generated-SDK helpers |
1853
+ | `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 |
1854
+ | `honey/proxy` | Registers `.proxy()` on route builders (import once); `ProxyConfig` type |
1855
+ | `honey/realtime` | Registers `app.realtime()` (import once) |
1856
+ | `honey/trust` | Registers hop counts and address lists for `app.trustProxy()` (import once) |
1857
+ | `honey/input` | `readableStream()` |
1858
+ | `honey/testing` | `testClient` |
1859
+ | `honey/ws/bun` `honey/ws/node` `honey/ws/deno` `honey/ws/cloudflare` | WS adapters |
1860
+ | `honey/ws/session` | The websocket session runtime. Every honey adapter imports it; a custom `WSAdapter` in a bundled build imports it once |
1861
+ | `honey/accepts` `honey/cookie` `honey/cookie-sign` `honey/crypto` `honey/request-to-curl` | Utilities |
1862
+ | `honey/telemetry/otel` | `otelAdapter` |
1863
+ | `honey/codegen` | `generateOpenApi`, `generateSDK`, `generateManifest`, trees, sanitize |
1864
+ | `honey/codegen-go-cli` | `generateGoCLI` |
1865
+ | `honey/codegen/extract` | ts-morph chain extractor |
1866
+ | `honey/build` | `createBuildPlugin` |
1867
+ | `honey/tree` | Tree types + `mergeTree` |
1868
+ | `honey/cli` | `honey` binary |
1869
+ | `honey/errors` | `defineErrors` (also on the root export) |
1557
1870
 
1558
1871
  ## Repository layout
1559
1872
 
@@ -1599,6 +1912,13 @@ bun run test:harness:rust # rust-only subset of the same loop
1599
1912
  bun run test:all # default suite + language harnesses
1600
1913
  ```
1601
1914
 
1915
+ The harness needs Go, cargo and `python3` on PATH. Python packages come from
1916
+ `packages/core/tests/python-requirements.txt`: on first run the harness creates
1917
+ `.cache/python-venv` and installs them there, then reuses it until the file changes.
1918
+ Set `HONEY_PYTHON` to use your own interpreter instead. Without a usable Python
1919
+ (no `python3`, no `venv` module, or offline on first run) the Python tests skip
1920
+ with a warning; `HONEY_REQUIRE_PYTHON=1` (set in CI) turns that into a failure.
1921
+
1602
1922
  Typecheck stays strict. Do not weaken `strict` or add `as any` to make it pass.
1603
1923
 
1604
1924
  ### Test matrix
@@ -1661,6 +1981,8 @@ bun run --filter @honey/bench bench:node
1661
1981
 
1662
1982
  Numbers and bundle sizes: [`bench/RESULTS.md`](https://github.com/lovrozagar/honey/blob/main/bench/RESULTS.md).
1663
1983
 
1984
+ `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.
1985
+
1664
1986
  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`.
1665
1987
 
1666
1988
  ## Releases