@lovrozagar/honey 0.6.5 → 0.7.0

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 +410 -149
  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 +776 -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 +1893 -1761
  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 +271 -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 +562 -424
  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/src/tree.ts CHANGED
@@ -1,92 +1,146 @@
1
+ import type { RouteId, Segment } from "./pattern.ts"
2
+ import { canonical, expandOptional, parsePattern, routeId } from "./pattern.ts"
1
3
  import type { HttpMethod, InputSchemasDef, OutputSchemaDef } from "./types.ts"
2
4
  import type { WSHandler } from "./ws/cloudflare.ts"
5
+ import type { WSOriginPolicy } from "./ws-origin.ts"
3
6
 
4
- export type { HttpMethod }
7
+ export type { HttpMethod, RouteId }
5
8
 
6
9
  export type OutputValidator = (statusKey: string, data: unknown) => Promise<void>
7
10
 
8
- export type RouteHandler = {
9
- /** cached compiled middleware chain (set lazily at first request) */
10
- _compiled?: (ctx: object) => Response | Promise<Response>
11
- /** internal route — excluded from codegen output */
11
+ type ChainMiddleware = (
12
+ ctx: Record<string, unknown>,
13
+ next: (additions?: Record<string, unknown>) => Promise<Response>,
14
+ ) => Promise<Response>
15
+
16
+ /**
17
+ * What a route was registered with, before the app resolves it. Finalize derives the
18
+ * resolved fields (`ek`, `mt`, `mw`, `bek`, `ef`) from these, so they never depend on the
19
+ * order of registration and are recomputed whenever the route graph changes.
20
+ */
21
+ type RecordSource = {
22
+ /** chain middleware captured at registration (plus any mounting chain) */
23
+ cm?: ChainMiddleware[]
24
+ /** `.context()` values of the registering handle (merged with the mounting handle's) */
25
+ cv?: Record<string, unknown> | null
26
+ /** error keys the route declared itself (`.errors()`, `.boundary()`) */
27
+ dk?: Set<string>
28
+ /** the settings of the app the route was registered on — opaque here */
29
+ own?: unknown
30
+ /** route-level boundary key (`.boundary()`) */
31
+ rb?: string | null
32
+ /** route-level middleware (`.get(...).use(mw)`) */
33
+ rm?: ChainMiddleware[]
34
+ /** explicit meta: chain `.meta()` overlaid by route `.meta()` */
35
+ xm?: Record<string, unknown> | null
36
+ }
37
+
38
+ /**
39
+ * A route record — everything one registered route runs. Records live in a per-app table
40
+ * keyed by `RouteId`; the tree only holds ids, so two apps that load the same tree never
41
+ * share a record.
42
+ */
43
+ export type RouteHandler = RecordSource & {
44
+ /** internal route (spec, docs, manifest) — excluded from codegen output, never mounted into a parent */
12
45
  _skip?: boolean
13
46
  /** boundary error key — wraps undeclared/unexpected errors (null = use default or internal_server_error) */
14
47
  bek: string | null
15
- /** pre-computed error factory subset (omit/`null` = use global factory) */
48
+ /** gateway catch-all: dispatches delegated routes, never matched for a path of its own */
49
+ ca?: boolean
50
+ /** delegated route: a tree leaf with no local handler, served by the gateway catch-all */
51
+ dl?: boolean
52
+ /** resolved error factory subset exposed as `ctx.errors` (omit/`null` = none) */
16
53
  ef?: Record<string, (...args: never[]) => unknown> | null
17
- /** declared error keys for this route — used for runtime enforcement */
54
+ /** declared error keys — resolved at finalize from the route, its middleware and the app defaults */
18
55
  ek: Set<string>
56
+ /** resolved error factory — the route's own app's, or the serving app's */
57
+ fac?: Record<string, (...args: never[]) => unknown> | null
19
58
  /** handler function */
20
59
  fn: (ctx: unknown) => Response | Promise<Response>
21
- /** input validation schemas — intern trees omit; missing/`null` = no validation */
60
+ /** route identity: `METHOD /canonical/pattern` */
61
+ id?: RouteId
62
+ /** input validation schemas — missing/`null` = no validation */
22
63
  iv?: InputSchemasDef | null
23
64
  /** route metadata — frozen object, accessible via ctx.meta */
24
65
  mt: Record<string, unknown> | null
25
- /** middleware chain — same as RuntimeMiddleware, inlined to avoid circular dep */
26
- mw: Array<
27
- (
28
- ctx: Record<string, unknown>,
29
- next: (additions?: Record<string, unknown>) => Promise<Response>,
30
- ) => Promise<Response>
31
- >
32
- /** output schemas by content-type — intern trees omit; missing/`null` = no validation */
66
+ /** resolved middleware chain, in run order: chain, scoped, route */
67
+ mw: ChainMiddleware[]
68
+ /** output schemas by content-type — missing/`null` = no validation */
33
69
  os?: OutputSchemaDef | null
34
70
  /** output validator function — validates response body against schema */
35
71
  ov?: OutputValidator | null
36
- /** registered path pattern (e.g. "/users/:id"); intern trees omit this */
72
+ /** canonical route pattern (e.g. "/users/:id") */
37
73
  rp?: string
38
74
  }
39
75
 
40
76
  export type { OutputSchemaDef }
41
77
 
42
- export type WSRouteHandler = {
78
+ export type WSRouteHandler = RecordSource & {
43
79
  bek: string | null
44
80
  ek: Set<string>
81
+ ef?: Record<string, (...args: never[]) => unknown> | null
82
+ fac?: Record<string, (...args: never[]) => unknown> | null
45
83
  fn: WSHandler<unknown>
84
+ id?: RouteId
46
85
  iv: InputSchemasDef | null
47
86
  mt: Record<string, unknown> | null
48
- mw: Array<
49
- (
50
- ctx: Record<string, unknown>,
51
- next: (additions?: Record<string, unknown>) => Promise<Response>,
52
- ) => Promise<Response>
53
- >
54
- /** registered ws path pattern — used by scoped-mw prefix filter */
87
+ mw: ChainMiddleware[]
88
+ /** browser origins allowed to open the socket (`.origins()`); `null` = same-origin when credentialed */
89
+ og?: WSOriginPolicy | null
90
+ /** canonical ws path pattern */
55
91
  rp: string
56
92
  }
57
93
 
94
+ /**
95
+ * Router topology. Leaves are `RouteId`s, never handler objects: what a leaf runs is looked
96
+ * up in the serving app's own route table.
97
+ */
58
98
  export type TreeNode = {
59
99
  /** dynamic param child — { n: param name, c: child subtree } */
60
100
  d: { c: TreeNode; n: string } | null
61
- /** route-graph generation — bumped on insert/merge so served specs can invalidate */
62
- g?: number
63
- /** method handlers — maps HTTP method (or ALL) to route handler */
64
- m: Record<HttpMethod | "ALL", RouteHandler> | null
65
- /** static children — maps literal path segment to child subtree */
101
+ /** method → route id at this exact path */
102
+ m: Record<string, RouteId> | null
103
+ /** static children — maps literal path segment to child subtree (null-prototype) */
66
104
  s: Record<string, TreeNode>
67
- /** wildcard handler — { n: wildcard name, m: method→handler map } */
68
- w: { m: Record<HttpMethod | "ALL", RouteHandler>; n: string } | null
69
- /** websocket handler for this path */
70
- ws: WSRouteHandler | null
105
+ /** wildcard — { n: wildcard name, m: method → route id } */
106
+ w: { m: Record<string, RouteId>; n: string } | null
107
+ /** websocket route id for this path */
108
+ ws: RouteId | null
71
109
  }
72
110
 
73
- export function bumpGeneration(root: TreeNode): number {
74
- const next = (root.g ?? 0) + 1
75
- root.g = next
76
- return next
111
+ /** Descriptive data for one route in a tree. Generated files carry only this. */
112
+ export type RouteEntry = {
113
+ /** boundary error key */
114
+ bek?: string | null
115
+ /** declared error keys */
116
+ ek?: readonly string[]
117
+ /** live record — only in `app.toRouteTree()` snapshots, never in generated files */
118
+ h?: RouteHandler | WSRouteHandler
119
+ /** internal route (spec, docs, manifest) */
120
+ i?: 1 | true
121
+ /** input schemas — only in live snapshots */
122
+ iv?: InputSchemasDef | null
123
+ /** route meta */
124
+ mt?: Record<string, unknown> | null
125
+ /** output schemas — only in live snapshots */
126
+ os?: OutputSchemaDef | null
77
127
  }
78
128
 
79
129
  export type MatchResult =
80
130
  | { allowed: HttpMethod[]; matched: false }
81
- | { handler: RouteHandler; matched: true; params: Record<string, string> }
131
+ | { id: RouteId; matched: true; params: Record<string, string> }
82
132
  | null
83
133
 
84
134
  export interface RouteMeta {}
85
135
 
136
+ /** Format of route trees: topology + per-route data. */
137
+ export const ROUTE_TREE_VERSION = 2
138
+
86
139
  export type RouteTree = {
87
- handlers?: Record<string, RouteHandler>
88
140
  meta: Record<string, RouteMeta>
89
141
  root: TreeNode
142
+ routes: Record<RouteId, RouteEntry>
143
+ v?: number
90
144
  }
91
145
 
92
146
  function decode(s: string): string {
@@ -98,385 +152,403 @@ function decode(s: string): string {
98
152
  }
99
153
  }
100
154
 
155
+ function dict<T>(): Record<string, T> {
156
+ return Object.create(null) as Record<string, T>
157
+ }
158
+
101
159
  export function createNode(): TreeNode {
102
- return {
103
- d: null,
104
- m: null,
105
- s: Object.create(null) as Record<string, TreeNode>,
106
- w: null,
107
- ws: null,
108
- }
160
+ return { d: null, m: null, s: dict<TreeNode>(), w: null, ws: null }
109
161
  }
110
162
 
111
- function splitSegments(path: string): string[] {
112
- return path.split("/").filter((s) => s.length > 0)
163
+ function describe(method: string, path: string): string {
164
+ return method === "WS" ? `WebSocket route ${path}` : `route: ${method} ${path}`
113
165
  }
114
166
 
115
- export function insertRoute(root: TreeNode, method: HttpMethod | "ALL", path: string, handler: RouteHandler): void {
116
- bumpGeneration(root)
117
- const segments = splitSegments(path)
167
+ /**
168
+ * Put `id` at the leaf for `segments` × `method` (`"WS"` for the websocket slot). The same id
169
+ * already there is a no-op; any other id is a duplicate. Optional params must be expanded by
170
+ * the caller — every call places exactly one leaf.
171
+ */
172
+ export function insertLeaf(root: TreeNode, segments: readonly Segment[], method: string, id: RouteId): void {
173
+ const path = canonical(segments)
118
174
  let node = root
119
-
120
175
  for (const seg of segments) {
121
- if (seg.startsWith("*")) {
122
- const name = seg.length > 1 ? seg.slice(1) : "*"
123
- if (node.w !== null && node.w.n !== name) {
124
- throw new Error(`Wildcard name conflict: "${node.w.n}" vs "${name}"`)
125
- }
126
- if (node.w === null) {
127
- node.w = {
128
- m: Object.create(null) as Record<HttpMethod | "ALL", RouteHandler>,
129
- n: name,
130
- }
131
- }
132
- if (method in node.w.m) {
133
- throw new Error(`Duplicate route: ${method} ${path}`)
176
+ if (seg.k === "wildcard") {
177
+ if (method === "WS") throw new Error("Wildcard segments not supported for WebSocket routes")
178
+ if (node.w !== null && node.w.n !== seg.n) {
179
+ throw new Error(`Wildcard name conflict: "${node.w.n}" vs "${seg.n}"`)
134
180
  }
135
- node.w.m[method] = handler
181
+ if (node.w === null) node.w = { m: dict<RouteId>(), n: seg.n }
182
+ const prev = node.w.m[method]
183
+ if (prev !== undefined && prev !== id) throw new Error(`Duplicate ${describe(method, path)}`)
184
+ node.w.m[method] = id
136
185
  return
137
186
  }
138
-
139
- if (seg.startsWith(":")) {
140
- const name = seg.endsWith("?") ? seg.slice(1, -1) : seg.slice(1)
141
- if (node.d !== null && node.d.n !== name) {
142
- throw new Error(`Route "${path}": param name conflict — expected ":${node.d.n}" but got ":${name}"`)
143
- }
144
- if (node.d === null) {
145
- node.d = { c: createNode(), n: name }
187
+ if (seg.k === "param") {
188
+ if (node.d !== null && node.d.n !== seg.n) {
189
+ throw new Error(`Route "${path}": param name conflict — expected ":${node.d.n}" but got ":${seg.n}"`)
146
190
  }
147
-
148
- if (seg.endsWith("?")) {
149
- /* optional param — register handler on parent too */
150
- if (node.m === null) {
151
- node.m = Object.create(null) as Record<HttpMethod | "ALL", RouteHandler>
152
- }
153
- if (method in node.m) {
154
- throw new Error(`Duplicate route: ${method} ${path}`)
155
- }
156
- node.m[method] = handler
157
- }
158
-
191
+ if (node.d === null) node.d = { c: createNode(), n: seg.n }
159
192
  node = node.d.c
160
193
  continue
161
194
  }
162
-
163
- /* static segment */
164
- const staticChild = node.s[seg]
165
- if (staticChild !== undefined) {
166
- node = staticChild
167
- continue
195
+ let child = node.s[seg.v]
196
+ if (child === undefined) {
197
+ child = createNode()
198
+ node.s[seg.v] = child
168
199
  }
169
- const nextNode = createNode()
170
- node.s[seg] = nextNode
171
- node = nextNode
172
- }
173
-
174
- /* terminal node */
175
- if (node.m === null) {
176
- node.m = Object.create(null) as Record<HttpMethod | "ALL", RouteHandler>
200
+ node = child
177
201
  }
178
- if (method in node.m) {
179
- throw new Error(`Duplicate route: ${method} ${path}`)
202
+ if (method === "WS") {
203
+ if (node.ws !== null && node.ws !== id) throw new Error(`Duplicate WebSocket route: ${path}`)
204
+ node.ws = id
205
+ return
180
206
  }
181
- node.m[method] = handler
207
+ if (node.m === null) node.m = dict<RouteId>()
208
+ const prev = node.m[method]
209
+ if (prev !== undefined && prev !== id) throw new Error(`Duplicate ${describe(method, path)}`)
210
+ node.m[method] = id
182
211
  }
183
212
 
184
- function resolveMethod(
185
- handlers: Record<HttpMethod | "ALL", RouteHandler>,
186
- method: HttpMethod,
187
- params: Record<string, string>,
188
- ): MatchResult {
189
- if (method in handlers) {
190
- return { handler: handlers[method], matched: true, params }
191
- }
192
- /* HEAD falls back to GET handler */
193
- if (method === "HEAD" && "GET" in handlers) {
194
- return { handler: handlers["GET"], matched: true, params }
195
- }
196
- if ("ALL" in handlers) {
197
- return { handler: handlers["ALL"], matched: true, params }
213
+ /**
214
+ * Place a route's leaves by pattern (an optional last param places two). The leaf holds
215
+ * `id`, by default the route's `RouteId`, which is returned.
216
+ */
217
+ export function insertRoute(root: TreeNode, method: HttpMethod | "ALL", path: string, id?: RouteId): RouteId {
218
+ const segments = parsePattern(path)
219
+ const rid = id ?? routeId(method, canonical(segments))
220
+ for (const v of expandOptional(segments)) {
221
+ if (findLeaf(root, v, method) !== undefined) throw new Error(`Duplicate route: ${method} ${canonical(v)}`)
222
+ insertLeaf(root, v, method, rid)
198
223
  }
199
- const allowed = Object.keys(handlers).filter((k) => k !== "ALL") as HttpMethod[]
200
- return { allowed, matched: false }
224
+ return rid
201
225
  }
202
226
 
203
- export function matchRoute(root: TreeNode, method: HttpMethod, path: string): MatchResult {
204
- let node = root
205
- const params: Record<string, string> = Object.create(null)
206
- const len = path.length
207
-
208
- /* skip leading slash */
209
- let pos = path.charCodeAt(0) === 47 ? 1 : 0
210
-
211
- while (pos < len) {
212
- /* find next slash or end */
213
- let end = pos
214
- while (end < len && path.charCodeAt(end) !== 47) end++
215
- if (end === pos) {
216
- pos++
217
- continue
218
- }
219
- const seg = path.substring(pos, end)
220
- pos = end + 1
227
+ /** Place a websocket route's leaf by pattern; returns its `RouteId`. */
228
+ export function insertWsRoute(root: TreeNode, path: string, id?: RouteId): RouteId {
229
+ const segments = parsePattern(path)
230
+ const rid = id ?? routeId("WS", canonical(segments))
231
+ for (const v of expandOptional(segments)) {
232
+ if (findLeaf(root, v, "WS") !== undefined) throw new Error(`Duplicate WebSocket route: ${canonical(v)}`)
233
+ insertLeaf(root, v, "WS", rid)
234
+ }
235
+ return rid
236
+ }
221
237
 
222
- /* 1. static child */
223
- const staticChild = node.s[seg]
224
- if (staticChild !== undefined) {
225
- node = staticChild
226
- continue
238
+ /** Exact lookup: the id at the leaf for `segments` × `method`, without matching params against values. */
239
+ export function findLeaf(root: TreeNode, segments: readonly Segment[], method: string): RouteId | undefined {
240
+ let node: TreeNode | undefined = root
241
+ for (const seg of segments) {
242
+ if (node === undefined) return undefined
243
+ if (seg.k === "wildcard") {
244
+ if (node.w === null || node.w.n !== seg.n) return undefined
245
+ return node.w.m[method]
227
246
  }
228
-
229
- /* 2. dynamic param */
230
- if (node.d !== null) {
231
- params[node.d.n] = decode(seg)
247
+ if (seg.k === "param") {
248
+ if (node.d === null || node.d.n !== seg.n) return undefined
232
249
  node = node.d.c
233
250
  continue
234
251
  }
235
-
236
- /* 3. wildcard — consumes remaining */
237
- if (node.w !== null) {
238
- params[node.w.n] = decode(pos <= len ? `${seg}/${path.substring(pos)}` : seg)
239
- return resolveMethod(node.w.m, method, params)
240
- }
241
-
242
- return null
243
- }
244
-
245
- /* terminal node */
246
- if (node.m !== null) {
247
- return resolveMethod(node.m, method, params)
252
+ node = node.s[seg.v]
248
253
  }
254
+ if (node === undefined) return undefined
255
+ if (method === "WS") return node.ws ?? undefined
256
+ return node.m?.[method]
257
+ }
249
258
 
250
- /* check wildcard for empty remainder */
251
- if (node.w !== null) {
252
- params[node.w.n] = ""
253
- return resolveMethod(node.w.m, method, params)
259
+ function pickMethod(m: Record<string, RouteId>, method: string): RouteId | undefined {
260
+ const exact = m[method]
261
+ if (exact !== undefined) return exact
262
+ if (method === "HEAD") {
263
+ const get = m["GET"]
264
+ if (get !== undefined) return get
254
265
  }
266
+ return m["ALL"]
267
+ }
255
268
 
256
- return null
269
+ type MatchState = {
270
+ allowed: Set<string> | null
271
+ method: string
272
+ names: string[]
273
+ path: string
274
+ values: string[]
257
275
  }
258
276
 
259
- export function insertWsRoute(root: TreeNode, path: string, handler: WSRouteHandler): void {
260
- bumpGeneration(root)
261
- const segments = splitSegments(path)
262
- let node = root
277
+ function addAllowed(st: MatchState, m: Record<string, RouteId>): void {
278
+ if (st.allowed === null) st.allowed = new Set()
279
+ for (const k in m) if (k !== "ALL") st.allowed.add(k)
280
+ }
263
281
 
264
- for (const seg of segments) {
265
- if (seg.startsWith("*")) {
266
- throw new Error("Wildcard segments not supported for WebSocket routes")
282
+ /*
283
+ * Depth-first in precedence order — static, then param, then wildcard — retrying the next
284
+ * branch on a dead end and on a method miss. A trie node sits at one depth, so each node is
285
+ * visited at most once per request: the walk is linear in the route set, and recursion depth
286
+ * is bounded by the deepest registered route, not by the request path.
287
+ */
288
+ function walk(node: TreeNode, pos: number, st: MatchState): RouteId | undefined {
289
+ const path = st.path
290
+ const len = path.length
291
+ while (pos < len && path.charCodeAt(pos) === 47) pos++
292
+ if (pos >= len) {
293
+ if (node.m !== null) {
294
+ const id = pickMethod(node.m, st.method)
295
+ if (id !== undefined) return id
296
+ addAllowed(st, node.m)
267
297
  }
268
-
269
- if (seg.startsWith(":")) {
270
- const name = seg.slice(1)
271
- if (node.d !== null && node.d.n !== name) {
272
- throw new Error(`Route "${path}": param name conflict — expected ":${node.d.n}" but got ":${name}"`)
273
- }
274
- if (node.d === null) {
275
- node.d = { c: createNode(), n: name }
298
+ if (node.w !== null) {
299
+ const id = pickMethod(node.w.m, st.method)
300
+ if (id !== undefined) {
301
+ st.names.push(node.w.n)
302
+ st.values.push("")
303
+ return id
276
304
  }
277
- node = node.d.c
278
- continue
305
+ addAllowed(st, node.w.m)
279
306
  }
280
-
281
- const staticChild = node.s[seg]
282
- if (staticChild !== undefined) {
283
- node = staticChild
284
- continue
307
+ return undefined
308
+ }
309
+ let end = path.indexOf("/", pos)
310
+ if (end === -1) end = len
311
+ const seg = path.substring(pos, end)
312
+
313
+ const staticChild = node.s[seg]
314
+ if (staticChild !== undefined) {
315
+ const id = walk(staticChild, end, st)
316
+ if (id !== undefined) return id
317
+ }
318
+ if (node.d !== null) {
319
+ const depth = st.names.length
320
+ st.names.push(node.d.n)
321
+ st.values.push(decode(seg))
322
+ const id = walk(node.d.c, end, st)
323
+ if (id !== undefined) return id
324
+ st.names.length = depth
325
+ st.values.length = depth
326
+ }
327
+ if (node.w !== null) {
328
+ const id = pickMethod(node.w.m, st.method)
329
+ if (id !== undefined) {
330
+ st.names.push(node.w.n)
331
+ st.values.push(decode(path.substring(pos)))
332
+ return id
285
333
  }
286
- const nextNode = createNode()
287
- node.s[seg] = nextNode
288
- node = nextNode
334
+ addAllowed(st, node.w.m)
289
335
  }
336
+ return undefined
337
+ }
290
338
 
291
- if (node.ws !== null) {
292
- throw new Error(`Duplicate WebSocket route: ${path}`)
339
+ function buildParams(st: MatchState): Record<string, string> {
340
+ const params = dict<string>()
341
+ for (let i = 0; i < st.names.length; i++) params[st.names[i]] = st.values[i]
342
+ return params
343
+ }
344
+
345
+ export function matchRoute(root: TreeNode, method: HttpMethod | string, path: string): MatchResult {
346
+ const st: MatchState = { allowed: null, method, names: [], path, values: [] }
347
+ const id = walk(root, 0, st)
348
+ if (id !== undefined) return { id, matched: true, params: buildParams(st) }
349
+ if (st.allowed !== null && st.allowed.size > 0) {
350
+ return { allowed: [...st.allowed] as HttpMethod[], matched: false }
293
351
  }
294
- node.ws = handler
352
+ return null
295
353
  }
296
354
 
297
- export function matchWsRoute(
298
- root: TreeNode,
299
- path: string,
300
- ): { handler: WSRouteHandler; params: Record<string, string> } | null {
301
- let node = root
302
- const params: Record<string, string> = Object.create(null)
355
+ function walkWs(node: TreeNode, pos: number, st: MatchState): RouteId | undefined {
356
+ const path = st.path
303
357
  const len = path.length
358
+ while (pos < len && path.charCodeAt(pos) === 47) pos++
359
+ if (pos >= len) return node.ws ?? undefined
360
+ let end = path.indexOf("/", pos)
361
+ if (end === -1) end = len
362
+ const seg = path.substring(pos, end)
363
+ const staticChild = node.s[seg]
364
+ if (staticChild !== undefined) {
365
+ const id = walkWs(staticChild, end, st)
366
+ if (id !== undefined) return id
367
+ }
368
+ if (node.d !== null) {
369
+ const depth = st.names.length
370
+ st.names.push(node.d.n)
371
+ st.values.push(decode(seg))
372
+ const id = walkWs(node.d.c, end, st)
373
+ if (id !== undefined) return id
374
+ st.names.length = depth
375
+ st.values.length = depth
376
+ }
377
+ return undefined
378
+ }
304
379
 
305
- let pos = path.charCodeAt(0) === 47 ? 1 : 0
380
+ export function matchWsRoute(root: TreeNode, path: string): { id: RouteId; params: Record<string, string> } | null {
381
+ const st: MatchState = { allowed: null, method: "WS", names: [], path, values: [] }
382
+ const id = walkWs(root, 0, st)
383
+ if (id === undefined) return null
384
+ return { id, params: buildParams(st) }
385
+ }
306
386
 
307
- while (pos < len) {
308
- let end = pos
309
- while (end < len && path.charCodeAt(end) !== 47) end++
310
- if (end === pos) {
311
- pos++
312
- continue
313
- }
314
- const seg = path.substring(pos, end)
315
- pos = end + 1
387
+ /** True when the tree holds at least one websocket leaf. */
388
+ export function hasWsLeaf(node: TreeNode): boolean {
389
+ if (node.ws !== null) return true
390
+ for (const k in node.s) if (hasWsLeaf(node.s[k])) return true
391
+ return node.d !== null && hasWsLeaf(node.d.c)
392
+ }
316
393
 
317
- const staticChild = node.s[seg]
318
- if (staticChild !== undefined) {
319
- node = staticChild
320
- continue
321
- }
394
+ /** Every leaf, with its concrete path pattern (optional params already expanded by insertion). */
395
+ export function forEachLeaf(
396
+ node: TreeNode,
397
+ cb: (method: string, path: string, id: RouteId) => void,
398
+ prefix = "",
399
+ ): void {
400
+ if (node.m !== null) for (const method in node.m) cb(method, prefix || "/", node.m[method])
401
+ if (node.ws !== null) cb("WS", prefix || "/", node.ws)
402
+ for (const seg in node.s) forEachLeaf(node.s[seg], cb, `${prefix}/${seg}`)
403
+ if (node.d !== null) forEachLeaf(node.d.c, cb, `${prefix}/:${node.d.n}`)
404
+ if (node.w !== null) {
405
+ const w = node.w.n === "*" ? "*" : `*${node.w.n}`
406
+ for (const method in node.w.m) cb(method, `${prefix}/${w}`, node.w.m[method])
407
+ }
408
+ }
322
409
 
323
- if (node.d !== null) {
324
- params[node.d.n] = decode(seg)
325
- node = node.d.c
326
- continue
327
- }
410
+ /** Deep copy into fresh null-prototype nodes. */
411
+ export function cloneTree(node: TreeNode): TreeNode {
412
+ const out = createNode()
413
+ for (const seg in node.s) out.s[seg] = cloneTree(node.s[seg])
414
+ if (node.m !== null) out.m = Object.assign(dict<RouteId>(), node.m)
415
+ if (node.d !== null) out.d = { c: cloneTree(node.d.c), n: node.d.n }
416
+ if (node.w !== null) out.w = { m: Object.assign(dict<RouteId>(), node.w.m), n: node.w.n }
417
+ out.ws = node.ws
418
+ return out
419
+ }
328
420
 
329
- return null
421
+ /** Deep-freeze a loaded tree so no app can mutate a module other apps also import. */
422
+ export function freezeTree(node: TreeNode): TreeNode {
423
+ if (Object.isFrozen(node)) return node
424
+ for (const seg in node.s) freezeTree(node.s[seg])
425
+ Object.freeze(node.s)
426
+ if (node.m !== null) Object.freeze(node.m)
427
+ if (node.d !== null) {
428
+ freezeTree(node.d.c)
429
+ Object.freeze(node.d)
330
430
  }
331
-
332
- if (node.ws !== null) {
333
- return { handler: node.ws, params }
431
+ if (node.w !== null) {
432
+ Object.freeze(node.w.m)
433
+ Object.freeze(node.w)
334
434
  }
335
-
336
- return null
435
+ return Object.freeze(node)
337
436
  }
338
437
 
339
- function mergeNodes(target: TreeNode, source: TreeNode, path: string): void {
340
- /* merge method handlers */
341
- if (source.m !== null && source.m !== undefined) {
342
- if (target.m === null || target.m === undefined) {
343
- target.m = Object.create(null) as Record<HttpMethod | "ALL", RouteHandler>
344
- }
345
- for (const [method, handler] of Object.entries(source.m)) {
346
- const prev = target.m[method as HttpMethod | "ALL"]
347
- if (prev !== undefined) {
348
- if (prev._skip && handler._skip) continue
349
- throw new Error(`Merge conflict: duplicate ${method} ${path}`)
350
- }
351
- target.m[method as HttpMethod | "ALL"] = handler
438
+ /**
439
+ * Reject a tree whose leaves are not route ids — a `routes.gen.ts` written by an older honey
440
+ * holds handler objects there.
441
+ */
442
+ export function assertTreeFormat(tree: { root?: unknown; routes?: unknown; v?: unknown }): void {
443
+ const stale = (): never => {
444
+ throw new Error(
445
+ "routeTree(): this route tree was generated by an older honey (leaves are handler objects, not route ids). Run `honey generate`.",
446
+ )
447
+ }
448
+ if (tree === null || typeof tree !== "object" || tree.root === null || typeof tree.root !== "object") stale()
449
+ if (tree.routes === null || typeof tree.routes !== "object") stale()
450
+ const check = (node: TreeNode): void => {
451
+ if (node.m !== null && typeof node.m === "object")
452
+ for (const k in node.m) if (typeof node.m[k] !== "string") stale()
453
+ if (node.w !== null && typeof node.w === "object") {
454
+ for (const k in node.w.m) if (typeof node.w.m[k] !== "string") stale()
352
455
  }
456
+ if (node.ws !== null && node.ws !== undefined && typeof node.ws !== "string") stale()
457
+ if (node.s === null || typeof node.s !== "object") stale()
458
+ for (const seg in node.s) check(node.s[seg])
459
+ if (node.d !== null && node.d !== undefined) check(node.d.c)
353
460
  }
461
+ check(tree.root as TreeNode)
462
+ }
354
463
 
355
- /* merge static children — shallow-copy s to avoid mutating shared empty objects */
356
- if (source.s && Object.keys(source.s).length > 0) {
357
- let detached = false
358
- for (const [seg, child] of Object.entries(source.s)) {
359
- const targetChild = target.s[seg]
360
- if (targetChild !== undefined) {
361
- mergeNodes(targetChild, child, `${path}/${seg}`)
362
- } else {
363
- if (!detached) {
364
- target.s = Object.assign(Object.create(null) as Record<string, TreeNode>, target.s)
365
- detached = true
366
- }
367
- target.s[seg] = child
368
- }
369
- }
464
+ function mergeMethodMap(
465
+ target: Record<string, RouteId>,
466
+ source: Record<string, RouteId>,
467
+ path: string,
468
+ ): Record<string, RouteId> {
469
+ for (const method in source) {
470
+ if (target[method] !== undefined) throw new Error(`Merge conflict: duplicate ${method} ${path}`)
471
+ target[method] = source[method]
370
472
  }
473
+ return target
474
+ }
371
475
 
372
- /* merge dynamic param */
373
- if (source.d !== null && source.d !== undefined) {
374
- if (target.d !== null && target.d !== undefined) {
476
+ function mergeNodes(target: TreeNode, source: TreeNode, path: string): void {
477
+ if (source.m !== null) {
478
+ if (target.m === null) target.m = dict<RouteId>()
479
+ mergeMethodMap(target.m, source.m, path || "/")
480
+ }
481
+ for (const seg in source.s) {
482
+ const child = source.s[seg]
483
+ const existing = target.s[seg]
484
+ if (existing !== undefined) mergeNodes(existing, child, `${path}/${seg}`)
485
+ else target.s[seg] = cloneTree(child)
486
+ }
487
+ if (source.d !== null) {
488
+ if (target.d !== null) {
375
489
  if (target.d.n !== source.d.n) {
376
490
  throw new Error(`Merge conflict: param name mismatch at ${path}: "${target.d.n}" vs "${source.d.n}"`)
377
491
  }
378
492
  mergeNodes(target.d.c, source.d.c, `${path}/:${target.d.n}`)
379
493
  } else {
380
- target.d = source.d
494
+ target.d = { c: cloneTree(source.d.c), n: source.d.n }
381
495
  }
382
496
  }
383
-
384
- /* merge wildcard */
385
- if (source.w !== null && source.w !== undefined) {
386
- if (target.w !== null && target.w !== undefined) {
497
+ if (source.w !== null) {
498
+ if (target.w !== null) {
387
499
  if (target.w.n !== source.w.n) {
388
500
  throw new Error(`Merge conflict: wildcard name mismatch at ${path}: "${target.w.n}" vs "${source.w.n}"`)
389
501
  }
390
- for (const [method, handler] of Object.entries(source.w.m)) {
391
- if (method in target.w.m) {
392
- throw new Error(`Merge conflict: duplicate ${method} ${path}/*${target.w.n}`)
393
- }
394
- target.w.m[method as HttpMethod | "ALL"] = handler
395
- }
502
+ mergeMethodMap(target.w.m, source.w.m, `${path}/*${target.w.n}`)
396
503
  } else {
397
- target.w = source.w
504
+ target.w = { m: Object.assign(dict<RouteId>(), source.w.m), n: source.w.n }
398
505
  }
399
506
  }
400
-
401
- /* merge websocket */
402
- if (source.ws !== null && source.ws !== undefined) {
403
- if (target.ws !== null && target.ws !== undefined) {
404
- throw new Error(`Merge conflict: duplicate WebSocket handler at ${path}`)
405
- }
507
+ if (source.ws !== null) {
508
+ if (target.ws !== null) throw new Error(`Merge conflict: duplicate WebSocket handler at ${path || "/"}`)
406
509
  target.ws = source.ws
407
510
  }
408
511
  }
409
512
 
410
- function withSchemas(
411
- target: Record<HttpMethod | "ALL", RouteHandler>,
412
- source: Record<HttpMethod | "ALL", RouteHandler>,
413
- ): Record<HttpMethod | "ALL", RouteHandler> {
414
- let next = target
415
- for (const [method, handler] of Object.entries(target) as Array<[HttpMethod | "ALL", RouteHandler]>) {
416
- const from = source[method]
417
- if (from === undefined) continue
418
- const iv = handler.iv ?? from.iv
419
- const os = handler.os ?? from.os
420
- if (iv === handler.iv && os === handler.os) continue
421
- /* intern trees share handler objects across routes — copy instead of mutating */
422
- if (next === target) next = { ...target }
423
- next[method] = { ...handler, iv, os }
424
- }
425
- return next
426
- }
427
-
428
- /**
429
- * Copy input/output schemas from `source` onto matching routes in `target` that lack them.
430
- * Generate-time only: a gateway app serves an intern tree (no schemas) while its merge source
431
- * (`mergeTree` over downstream `app.toRouteTree()`) still carries them for OpenAPI.
432
- */
433
- export function overlaySchemas(target: TreeNode, source: TreeNode): void {
434
- if (target.m !== null && source.m !== null) target.m = withSchemas(target.m, source.m)
435
- if (target.w !== null && source.w !== null) target.w = { ...target.w, m: withSchemas(target.w.m, source.w.m) }
436
- for (const [seg, child] of Object.entries(target.s)) {
437
- const from = source.s[seg]
438
- if (from !== undefined) overlaySchemas(child, from)
439
- }
440
- if (target.d !== null && source.d !== null) overlaySchemas(target.d.c, source.d.c)
441
- }
442
-
443
- /** Merge source tree nodes into target tree (mutates target) */
513
+ /** Merge `source` topology into `target` (mutates target; never shares source nodes). */
444
514
  export function mergeInto(target: TreeNode, source: TreeNode): void {
445
- bumpGeneration(target)
446
515
  mergeNodes(target, source, "")
447
516
  }
448
517
 
449
518
  type TreeInput = RouteTree | [RouteTree, Record<string, unknown>]
450
519
 
451
- function tagHandlerMeta(node: TreeNode, extra: Record<string, unknown>): void {
452
- if (node.m !== null) {
453
- for (const handler of Object.values(node.m) as RouteHandler[]) {
454
- handler.mt = handler.mt ? { ...handler.mt, ...extra } : { ...extra }
455
- }
456
- }
457
- for (const child of Object.values(node.s)) {
458
- tagHandlerMeta(child, extra)
459
- }
460
- if (node.d) tagHandlerMeta(node.d.c, extra)
461
- if (node.w !== null) {
462
- for (const handler of Object.values(node.w.m) as RouteHandler[]) {
463
- handler.mt = handler.mt ? { ...handler.mt, ...extra } : { ...extra }
464
- }
465
- }
520
+ function withExtraMeta(entry: RouteEntry, extra: Record<string, unknown> | undefined): RouteEntry {
521
+ if (extra === undefined) return { ...entry }
522
+ const mt = Object.freeze(entry.mt ? { ...entry.mt, ...extra } : { ...extra })
523
+ const out: RouteEntry = { ...entry, mt }
524
+ if (entry.h !== undefined) out.h = { ...entry.h, mt } as RouteHandler
525
+ return out
466
526
  }
467
527
 
528
+ /**
529
+ * Merge several trees into a new one. Inputs are never mutated; extra meta given per tree is
530
+ * applied to copies of its route entries.
531
+ */
468
532
  export function mergeTree(...trees: TreeInput[]): RouteTree {
469
- const merged: RouteTree = {
470
- meta: {},
471
- root: createNode(),
472
- }
473
-
474
- for (const entry of trees) {
475
- const [tree, extra] = Array.isArray(entry) ? entry : [entry, undefined]
476
- if (extra) tagHandlerMeta(tree.root, extra)
533
+ const merged: RouteTree = { meta: {}, root: createNode(), routes: dict<RouteEntry>(), v: ROUTE_TREE_VERSION }
534
+ for (const input of trees) {
535
+ const [tree, extra] = Array.isArray(input) ? input : [input, undefined]
536
+ assertTreeFormat(tree)
477
537
  mergeNodes(merged.root, tree.root, "")
538
+ for (const id in tree.routes) {
539
+ if (id in merged.routes) throw new Error(`Merge conflict: duplicate ${id}`)
540
+ merged.routes[id] = withExtraMeta(tree.routes[id], extra)
541
+ }
478
542
  Object.assign(merged.meta, tree.meta)
479
543
  }
480
-
481
544
  return merged
482
545
  }
546
+
547
+ /**
548
+ * Copy input/output schemas from `source` onto `target`'s routes that lack them.
549
+ * Generate-time only: a gateway app serves its generated tree (no schemas) while its merge
550
+ * source (`mergeTree` over downstream `app.toRouteTree()`) still carries them for OpenAPI.
551
+ */
552
+ export function overlaySchemas(target: { _overlaySchemas(source: RouteTree): void }, source: RouteTree): void {
553
+ target._overlaySchemas(source)
554
+ }