@lovrozagar/honey 0.1.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 (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
package/src/index.ts ADDED
@@ -0,0 +1,3843 @@
1
+ import { HoneyContext } from "./context.ts";
2
+ import { HoneyError } from "./error.ts";
3
+ import { ERROR_META } from "./errors.ts";
4
+ import type { MiddlewareFn, RuntimeMiddleware } from "./middleware.ts";
5
+ import { compileChain, executeChain } from "./middleware.ts";
6
+ import type { ProxyConfig } from "./proxy.ts";
7
+ import { createProxyHandler } from "./proxy.ts";
8
+ import type {
9
+ CustomErrorFormatter,
10
+ ErrorFormatter,
11
+ ResponseOptions,
12
+ TypedResponse,
13
+ } from "./response.ts";
14
+ import { createErrorResponse, type HoneyRes } from "./response.ts";
15
+ import type {
16
+ OutputValidator,
17
+ RouteHandler,
18
+ RouteTree,
19
+ TreeNode,
20
+ WSRouteHandler,
21
+ } from "./tree.ts";
22
+ import {
23
+ createNode,
24
+ insertRoute,
25
+ insertWsRoute,
26
+ matchRoute,
27
+ matchWsRoute,
28
+ mergeInto,
29
+ } from "./tree.ts";
30
+ import { createBus } from "./realtime/bus.ts";
31
+ import type { RealtimeBus } from "./realtime/bus.ts";
32
+ import { createConnContext } from "./realtime/route.ts";
33
+ import type { RealtimeRouteOpts } from "./realtime/route.ts";
34
+ import type {
35
+ ComputeErrorsByStatus,
36
+ DefaultMeta,
37
+ ExtractSchemas,
38
+ HttpMethod,
39
+ InferInputMap,
40
+ InferOutput,
41
+ InputSchemasDef,
42
+ MergePath,
43
+ MergeRoute,
44
+ OutputSchemaDef,
45
+ ParamsFromPath,
46
+ StandardSchemaLike,
47
+ StatusKey,
48
+ TapContext,
49
+ } from "./types.ts";
50
+ import { codeToStatusKey, EK, EMPTY_OBJ, SK } from "./types.ts";
51
+ import { validateInput, validateOutput } from "./validation.ts";
52
+ import type { WSAdapter, WSContext, WSHandler } from "./ws/cloudflare.ts";
53
+ import { loadHoneyFeature } from "./feature-load.ts";
54
+ import { getI18nRuntime } from "./i18n-slot.ts";
55
+ import { getOpenApiRuntime } from "./openapi/spec-factory.ts";
56
+ import { getServeRuntime } from "./serve-slot.ts";
57
+ import type { HoneyServeOptions, ServeHandle } from "./serve.ts";
58
+
59
+ export { HoneyContext } from "./context.ts";
60
+ /** HoneyContext without internal backing fields — use this for consumer-facing types */
61
+ export type HoneyCtx<TEnv = Record<string, unknown>> = Omit<
62
+ import("./context.ts").HoneyContext<TEnv>,
63
+ | "_errorToResponse"
64
+ | "_isErrorResponse"
65
+ | "_lzCookies"
66
+ | "_lzHeaders"
67
+ | "_lzSearch"
68
+ | "_lzSearchAll"
69
+ | "_lzUrlFn"
70
+ | "_setErrors"
71
+ >;
72
+ export { HoneyError } from "./error.ts";
73
+ export { defineErrors, ERROR_META } from "./errors.ts";
74
+ export type { ErrorMetaEntry } from "./errors.ts";
75
+ export type { MiddlewareFn } from "./middleware.ts";
76
+ export { createMiddleware, defineMiddleware } from "./middleware.ts";
77
+ export type { ProxyConfig } from "./proxy.ts";
78
+ export type { PendingTap, TapContext, TapHandler } from "./types.ts";
79
+ export type {
80
+ CookieOptions,
81
+ CustomErrorFormatter,
82
+ ErrorFormatter,
83
+ ResponseOptions,
84
+ SSEEvent,
85
+ SSEOptions,
86
+ SSEStream,
87
+ TypedResponse,
88
+ } from "./response.ts";
89
+ export { createErrorResponse, HoneyRes } from "./response.ts";
90
+ export { mergeTree } from "./tree.ts";
91
+ export type {
92
+ DefaultMeta,
93
+ HoneyCodegen,
94
+ HoneyMeta,
95
+ InferBasePath,
96
+ InferCtx,
97
+ InferEnv,
98
+ InferErrorFactory,
99
+ InferInputMap,
100
+ InferMeta,
101
+ InferMethods,
102
+ InferOutput,
103
+ InferRouteCtx,
104
+ InferRouteErrors,
105
+ InferRouteInput,
106
+ InferRouteMeta,
107
+ InferRouteMethods,
108
+ InferRouteOutput,
109
+ InferRoutePaths,
110
+ InferRoutes,
111
+ InputSchemasDef,
112
+ MergePath,
113
+ OpenApiMeta,
114
+ OutputSchemaDef,
115
+ Overwrite,
116
+ ComputeErrorsByStatus,
117
+ RouteRecord,
118
+ StandardSchemaLike,
119
+ StatusKey,
120
+ SuccessStatusKey,
121
+ } from "./types.ts";
122
+ export type { WSAdapter, WSContext, WSHandler, WSPreUpgrade } from "./ws/cloudflare.ts";
123
+ export type { ConnContext, RealtimeRouteOpts } from "./realtime/route.ts";
124
+ export type { RealtimeBus } from "./realtime/bus.ts";
125
+ export type { HoneyServeOptions, ServeHandle } from "./serve.ts";
126
+ export type { ServeRuntime } from "./detect-runtime.ts";
127
+ export { detectRuntime } from "./detect-runtime.ts";
128
+
129
+ /** Type predicate for incoming wire-protocol msg frames from the realtime client. */
130
+ function isMsgFrame(value: unknown): value is { data: unknown; t: "msg" } {
131
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
132
+ if (!("t" in value) || !("data" in value)) return false;
133
+ return value.t === "msg";
134
+ }
135
+
136
+ type TelemetryAdapter = {
137
+ onError?(ctx: {
138
+ duration: number;
139
+ error: HoneyError;
140
+ method: string;
141
+ path: string;
142
+ }): void;
143
+ onHandler?(ctx: {
144
+ duration: number;
145
+ method: string;
146
+ path: string;
147
+ status: number;
148
+ }): void;
149
+ onMethodNotAllowed?(ctx: {
150
+ allowed: string[];
151
+ method: string;
152
+ path: string;
153
+ req: Request;
154
+ }): void;
155
+ onMiddleware?(ctx: { duration: number; error?: unknown; name: string }): void;
156
+ onNotFound?(ctx: { method: string; path: string; req: Request }): void;
157
+ onRequest?(ctx: { env: unknown; req: Request }): void;
158
+ onResponse?(ctx: { duration: number; req: Request; status: number }): void;
159
+ onRoute?(ctx: {
160
+ method: string;
161
+ params: Record<string, string>;
162
+ path: string;
163
+ req: Request;
164
+ }): void;
165
+ };
166
+
167
+ type ErrorI18nConfig<TEnv> = {
168
+ errors?: Record<string, Record<string, string>>;
169
+ fieldNames?: Record<string, Record<string, string>>;
170
+ resolveLocale: (ctx: {
171
+ cookies: Record<string, string>;
172
+ env: TEnv;
173
+ headers: Record<string, string>;
174
+ params: Record<string, string>;
175
+ req: Request;
176
+ search: Record<string, string>;
177
+ }) => string | Promise<string>;
178
+ };
179
+
180
+ type Logger = {
181
+ warn?(msg: string, ...args: unknown[]): void;
182
+ };
183
+
184
+ function mergePath(base: string, path: string): string {
185
+ if (base === "/") return path;
186
+ if (path === "/") return base;
187
+ return `${base}${path}`;
188
+ }
189
+
190
+ /** Normalize a scope prefix: ensure leading slash, strip trailing slash (except "/"). */
191
+ function normalizeScopePath(raw: string): string {
192
+ let p = raw;
193
+ if (p.length === 0) return "/";
194
+ if (p.charCodeAt(0) !== 47) p = `/${p}`;
195
+ if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
196
+ return p;
197
+ }
198
+
199
+ /** Check whether fullPath falls under scope prefix — exact match or next char is '/'. */
200
+ function scopeMatches(prefix: string, fullPath: string): boolean {
201
+ if (prefix === "/") return true;
202
+ if (fullPath === prefix) return true;
203
+ if (fullPath.length <= prefix.length) return false;
204
+ if (fullPath.charCodeAt(prefix.length) !== 47) return false;
205
+ return fullPath.startsWith(prefix);
206
+ }
207
+
208
+ type HoneyGraph = {
209
+ handlerMap: Record<string, RouteHandler> | null
210
+ hasRouteTree: boolean
211
+ realtimeBus: RealtimeBus | null
212
+ root: TreeNode
213
+ }
214
+
215
+ function walkTreeHandlers(
216
+ root: TreeNode,
217
+ cb: (h: RouteHandler) => void,
218
+ wsCb?: (h: WSRouteHandler) => void,
219
+ ): void {
220
+ if (root.m) {
221
+ for (const h of Object.values(root.m) as RouteHandler[]) cb(h);
222
+ }
223
+ if (root.w) {
224
+ for (const h of Object.values(root.w.m) as RouteHandler[]) cb(h);
225
+ }
226
+ if (root.ws && wsCb) wsCb(root.ws);
227
+ for (const child of Object.values(root.s)) walkTreeHandlers(child, cb, wsCb);
228
+ if (root.d) walkTreeHandlers(root.d.c, cb, wsCb);
229
+ }
230
+
231
+ const EMPTY_PARAMS = EMPTY_OBJ as Record<string, string>
232
+ const EMPTY_MW: RuntimeMiddleware[] = []
233
+
234
+ /** Find first '?' or '#' in url starting from pos */
235
+ function findSearchOrHash(url: string, pos: number): number {
236
+ for (let i = pos; i < url.length; i++) {
237
+ const c = url.charCodeAt(i);
238
+ if (c === 63 || c === 35) return i;
239
+ }
240
+ return -1;
241
+ }
242
+
243
+ function safeFire(fn: (() => unknown) | undefined, logger?: Logger): void {
244
+ if (fn === undefined) return;
245
+ try {
246
+ const result = fn();
247
+ if (result && typeof result === "object" && "catch" in result) {
248
+ (result as Promise<unknown>).catch((e: unknown) => {
249
+ logger?.warn?.("telemetry callback failed", e);
250
+ });
251
+ }
252
+ } catch (e) {
253
+ logger?.warn?.("telemetry callback failed", e);
254
+ }
255
+ }
256
+
257
+ /** Internal context shared across extracted fetch sub-methods */
258
+ type FetchCtx<TEnv> = {
259
+ env: TEnv;
260
+ executionCtx: { waitUntil?: (p: Promise<unknown>) => void } | undefined;
261
+ log: Logger | undefined;
262
+ request: Request;
263
+ startTime: number;
264
+ url: () => URL;
265
+ };
266
+
267
+ function defaultErrorFormatter(
268
+ _error: HoneyError,
269
+ defaultShape: Record<string, unknown>,
270
+ ): Record<string, unknown> {
271
+ return defaultShape;
272
+ }
273
+
274
+ type ErrorFormatterFn = ErrorFormatter;
275
+
276
+ const STATIC_CTX_RESERVED = new Set([
277
+ "background",
278
+ "cookies",
279
+ "env",
280
+ "headers",
281
+ "params",
282
+ "req",
283
+ "res",
284
+ "search",
285
+ ]);
286
+
287
+ /* errorKeys the framework throws on its own behalf — input/output validation, content negotiation,
288
+ * routing. Always passes the boundary check; users never declare these via .errors(). */
289
+ const FRAMEWORK_EKS = new Set<(typeof EK)[keyof typeof EK]>([
290
+ EK.validation_failed,
291
+ EK.output_validation_failed,
292
+ EK.output_content_type_mismatch,
293
+ EK.unsupported_media_type,
294
+ EK.content_too_large,
295
+ EK.method_not_allowed,
296
+ EK.not_found,
297
+ EK.too_many_requests,
298
+ EK.request_timeout,
299
+ EK.gateway_timeout,
300
+ EK.bad_gateway,
301
+ ]);
302
+
303
+ export class Honey<
304
+ TEnv,
305
+ TCtx = HoneyContext<TEnv>,
306
+ TRoutes = {},
307
+ TMeta = never,
308
+ TErrorFactory = never,
309
+ TDefaultErrors extends string = never,
310
+ TBasePath extends string = "/",
311
+ TTaps extends Record<string, unknown> = {},
312
+ TScopedMw extends readonly ScopedMwEntry[] = [],
313
+ > {
314
+ declare readonly $basePath: TBasePath;
315
+ declare readonly $ctx: TCtx;
316
+ declare readonly $env: TEnv;
317
+ declare readonly $errorFactory: TErrorFactory;
318
+ declare readonly $meta: TMeta;
319
+ declare readonly $routes: TRoutes;
320
+ declare readonly $taps: TTaps;
321
+ private _basePath: string;
322
+ private _defaultBoundaryKey: string | null;
323
+ private _defaultErrorKeys: Set<string>;
324
+ private _errorFactory: unknown;
325
+ private _errorSchema: StandardSchemaLike | null;
326
+ private _customErrorFormatter: CustomErrorFormatter | null;
327
+ private _customErrorSchema: StandardSchemaLike | null;
328
+ private _chainMeta: Record<string, unknown> | null;
329
+ private _chainMiddlewares: RuntimeMiddleware[];
330
+ private _scopedMiddlewares: ScopedEntry[];
331
+ private _contextValues: Record<string, unknown> | null;
332
+ private _errorFormatter: ErrorFormatterFn;
333
+ private _errorI18n: ErrorI18nConfig<TEnv> | null;
334
+ private _globalMiddlewares: RuntimeMiddleware[];
335
+ private _graph: HoneyGraph;
336
+ private _hasWsRoutes: boolean;
337
+ private _logger: Logger | null;
338
+ private _outputValidation: "always" | "dev" | "off";
339
+ private _staticRoutes: { map: Record<string, RouteHandler> | null };
340
+ private _stripPrefix: string | null;
341
+ private _trailingSlash: "enforce" | "ignore" | "strip";
342
+ private _wsAdapter: WSAdapter | null;
343
+ private _openApiCache: { epoch: number; value: Promise<unknown> } | null;
344
+ private _openApiYamlCache: { epoch: number; value: Promise<string> } | null;
345
+ private _manifestCache: { epoch: number; value: Promise<unknown> } | null;
346
+ private _onError:
347
+ | ((
348
+ error: unknown,
349
+ ctx: {
350
+ env: TEnv;
351
+ jsonFromError: (err: HoneyError) => Response;
352
+ req: Request;
353
+ },
354
+ ) => HoneyError | Response | Promise<HoneyError | Response | undefined | void> | undefined | void)
355
+ | null;
356
+ private _onMethodNotAllowed:
357
+ | ((ctx: {
358
+ allowed: string[];
359
+ env: TEnv;
360
+ jsonFromError: (err: HoneyError) => Response;
361
+ req: Request;
362
+ }) => Response | Promise<Response>)
363
+ | null;
364
+ private _onNotFound:
365
+ | ((ctx: {
366
+ env: TEnv;
367
+ jsonFromError: (err: HoneyError) => Response;
368
+ req: Request;
369
+ }) => Response | Promise<Response>)
370
+ | null;
371
+ private _taps: Map<
372
+ string,
373
+ (ctx: TapContext<TEnv>, payload: unknown) => void | Promise<void>
374
+ > | null;
375
+ private _telemetry: TelemetryAdapter | null;
376
+ private _realtimeRoutes: Map<string, { handler: RealtimeRouteOpts["handler"]; middlewares?: RealtimeRouteOpts["use"]; reconnectBuffer?: number }>;
377
+
378
+ constructor(opts?: {
379
+ chainMiddlewares?: RuntimeMiddleware[];
380
+ defaultErrorKeys?: Set<string>;
381
+ globalMiddlewares?: RuntimeMiddleware[];
382
+ graph?: HoneyGraph;
383
+ handlerMap?: Record<string, RouteHandler> | null;
384
+ root?: TreeNode;
385
+ scopedMiddlewares?: ScopedEntry[];
386
+ }) {
387
+ this._basePath = "/";
388
+ this._graph = opts?.graph ?? {
389
+ handlerMap: opts?.handlerMap ?? null,
390
+ hasRouteTree: false,
391
+ realtimeBus: null,
392
+ root: opts?.root ?? createNode(),
393
+ };
394
+ this._globalMiddlewares = opts?.globalMiddlewares ?? [];
395
+ this._scopedMiddlewares = opts?.scopedMiddlewares ?? [];
396
+ this._chainMeta = null;
397
+ this._chainMiddlewares = opts?.chainMiddlewares ?? [];
398
+ this._contextValues = null;
399
+ this._defaultBoundaryKey = null;
400
+ this._defaultErrorKeys = opts?.defaultErrorKeys ?? new Set();
401
+ this._errorFactory = null;
402
+ this._errorSchema = null;
403
+ this._customErrorFormatter = null;
404
+ this._customErrorSchema = null;
405
+ this._errorFormatter = defaultErrorFormatter;
406
+ this._errorI18n = null;
407
+ this._hasWsRoutes = false;
408
+ this._logger = null;
409
+ this._outputValidation = "off";
410
+ this._staticRoutes = { map: null };
411
+ this._stripPrefix = null;
412
+ this._trailingSlash = "ignore";
413
+ this._wsAdapter = null;
414
+ this._openApiCache = null;
415
+ this._openApiYamlCache = null;
416
+ this._manifestCache = null;
417
+ this._onError = null;
418
+ this._onNotFound = null;
419
+ this._onMethodNotAllowed = null;
420
+ this._taps = null;
421
+ this._telemetry = null;
422
+ this._realtimeRoutes = new Map();
423
+ }
424
+
425
+ private get _handlerMap(): Record<string, RouteHandler> | null {
426
+ return this._graph.handlerMap;
427
+ }
428
+ private set _handlerMap(value: Record<string, RouteHandler> | null) {
429
+ this._graph.handlerMap = value;
430
+ }
431
+
432
+ private get _hasRouteTree(): boolean {
433
+ return this._graph.hasRouteTree;
434
+ }
435
+ private set _hasRouteTree(value: boolean) {
436
+ this._graph.hasRouteTree = value;
437
+ }
438
+
439
+ private get _root(): TreeNode {
440
+ return this._graph.root;
441
+ }
442
+ private set _root(value: TreeNode) {
443
+ this._graph.root = value;
444
+ }
445
+
446
+ /** @internal — used by codegen */
447
+ private get _tree(): TreeNode {
448
+ return this._root;
449
+ }
450
+
451
+ private get _realtimeBus(): RealtimeBus | null {
452
+ return this._graph.realtimeBus;
453
+ }
454
+ private set _realtimeBus(value: RealtimeBus | null) {
455
+ this._graph.realtimeBus = value;
456
+ }
457
+
458
+ /** @internal — mark that this app has WS routes */
459
+ _markWsRoutes(): void {
460
+ this._hasWsRoutes = true;
461
+ }
462
+
463
+ /** @internal — register a static route for O(1) lookup */
464
+ _registerStatic(key: string, handler: RouteHandler): void {
465
+ if (this._staticRoutes.map === null) {
466
+ this._staticRoutes.map = Object.create(null) as Record<string, RouteHandler>;
467
+ }
468
+ this._staticRoutes.map[key] = handler;
469
+ }
470
+
471
+ /** @internal — used by RouteBuilder for pre-filtered error factory */
472
+ get _factory(): unknown {
473
+ return this._errorFactory;
474
+ }
475
+
476
+ /** @internal — used by runtime error boundary */
477
+ get _boundaryKey(): string | null {
478
+ return this._defaultBoundaryKey;
479
+ }
480
+
481
+ /** Convert unknown thrown value to error Response — used in WS, 404, 405 catch blocks */
482
+ private _toErrorResponse(thrown: unknown): Response {
483
+ const error =
484
+ thrown instanceof HoneyError
485
+ ? thrown
486
+ : new HoneyError({
487
+ cause: thrown,
488
+ errorKey: EK.internal_server_error,
489
+ status: SK.internal_server_error,
490
+ });
491
+ return createErrorResponse(error, this._errorFormatter, this._customErrorFormatter);
492
+ }
493
+
494
+ private _createBoundaryError(errorKey: string, cause: unknown): HoneyError {
495
+ const factory = this._errorFactory as Record<
496
+ string,
497
+ ((opts?: { cause?: unknown }) => HoneyError) | undefined
498
+ > | null;
499
+ const factoryFn = factory?.[errorKey];
500
+ if (factoryFn) {
501
+ /* check if this is a custom schema error via ERROR_META — boundary must use standard errors only */
502
+ const meta = (factory as Record<symbol, Record<string, { schema: unknown }>>)?.[ERROR_META];
503
+ if (meta?.[errorKey]?.schema) {
504
+ /* custom schema error — cannot be used as boundary, fall through to manual construction */
505
+ } else {
506
+ return factoryFn({ cause });
507
+ }
508
+ }
509
+ return new HoneyError({
510
+ cause,
511
+ errorKey,
512
+ status: SK.internal_server_error,
513
+ });
514
+ }
515
+
516
+ private _createError(errorKey: string, statusKey: StatusKey): HoneyError {
517
+ const factory = this._errorFactory as Record<
518
+ string,
519
+ (() => HoneyError) | undefined
520
+ > | null;
521
+ const factoryFn = factory?.[errorKey];
522
+ if (factoryFn) {
523
+ return factoryFn();
524
+ }
525
+ return new HoneyError({ errorKey, status: statusKey });
526
+ }
527
+
528
+ /** Apply error keys from a single scoped entry to every matching handler currently in the tree */
529
+ private _applyScopedEntryErrors(entry: ScopedEntry): void {
530
+ const errors = entry.errors;
531
+ if (!errors || errors.length === 0) return;
532
+ walkTreeHandlers(
533
+ this._root,
534
+ (h) => {
535
+ if (scopeMatches(entry.prefix, h.rp)) {
536
+ for (const k of errors) h.ek.add(k);
537
+ }
538
+ },
539
+ (wh) => {
540
+ if (scopeMatches(entry.prefix, wh.rp)) {
541
+ for (const k of errors) wh.ek.add(k);
542
+ }
543
+ },
544
+ );
545
+ }
546
+
547
+ /** Apply error keys from every scoped entry on this chain to every matching handler in the tree */
548
+ private _applyAllScopedErrors(): void {
549
+ for (const entry of this._scopedMiddlewares) {
550
+ this._applyScopedEntryErrors(entry);
551
+ }
552
+ }
553
+
554
+ /** Return scoped middleware functions that match the given route path */
555
+ private _filterScopedForPath(routePath: string): RuntimeMiddleware[] {
556
+ if (this._scopedMiddlewares.length === 0) return EMPTY_MW;
557
+ const out: RuntimeMiddleware[] = [];
558
+ for (const s of this._scopedMiddlewares) {
559
+ if (scopeMatches(s.prefix, routePath)) out.push(s.mw);
560
+ }
561
+ return out;
562
+ }
563
+
564
+ /** Mutates `honeyError.message` in-place with the i18n-resolved template for its errorKey.
565
+ * No-op when i18n is not configured or when no template matches. */
566
+ private async _resolveI18n(
567
+ honeyError: HoneyError,
568
+ ctx: HoneyContext<TEnv>,
569
+ env: TEnv,
570
+ request: Request,
571
+ log?: Logger,
572
+ ): Promise<void> {
573
+ if (!this._errorI18n) return;
574
+ try {
575
+ const locale = await this._errorI18n.resolveLocale({
576
+ cookies: ctx.cookies,
577
+ env,
578
+ headers: ctx.headers,
579
+ params: ctx.params,
580
+ req: request,
581
+ search: ctx.search,
582
+ });
583
+ const translations = this._errorI18n.errors?.[locale];
584
+ if (translations) {
585
+ const template = translations[honeyError.errorKey];
586
+ if (template) {
587
+ await loadHoneyFeature("i18n")
588
+ honeyError.message = getI18nRuntime().interpolate(
589
+ template,
590
+ honeyError.vars ?? {},
591
+ );
592
+ }
593
+ }
594
+
595
+ const fieldTranslations = this._errorI18n.fieldNames?.[locale];
596
+ if (fieldTranslations && Object.keys(honeyError.fields).length > 0) {
597
+ for (const fieldErrors of Object.values(honeyError.fields)) {
598
+ for (const fe of fieldErrors) {
599
+ let candidate = fe.path;
600
+ while (candidate) {
601
+ const translated = fieldTranslations[candidate];
602
+ if (translated) {
603
+ fe.path = translated;
604
+ break;
605
+ }
606
+ const dotIdx = candidate.indexOf(".");
607
+ if (dotIdx === -1) break;
608
+ candidate = candidate.slice(dotIdx + 1);
609
+ }
610
+ }
611
+ }
612
+ }
613
+ } catch (e) {
614
+ log?.warn?.("i18n resolution failed", e);
615
+ }
616
+ }
617
+
618
+ /**
619
+ * Convert thrown value into an error Response — resolves boundary wrapping,
620
+ * i18n translation, onError callback, and telemetry.
621
+ * Called from the handler wrapper so errors flow back through middleware.
622
+ */
623
+ private async _resolveErrorResponse(
624
+ thrown: unknown,
625
+ handler: RouteHandler,
626
+ fc: FetchCtx<TEnv>,
627
+ method: string,
628
+ path: string,
629
+ ctx: HoneyContext<TEnv>,
630
+ ): Promise<Response> {
631
+ const { env, log, request, startTime } = fc;
632
+ let honeyError: HoneyError;
633
+ const boundaryKey = handler.bek ?? this._defaultBoundaryKey;
634
+
635
+ if (thrown instanceof HoneyError) {
636
+ /* framework-managed errorKeys (input/output validation, content negotiation, etc.) are always allowed
637
+ * regardless of handler.ek — users never declare them, the framework owns them. */
638
+ const isFrameworkEk = FRAMEWORK_EKS.has(thrown.errorKey as (typeof EK)[keyof typeof EK]);
639
+ if (!isFrameworkEk && handler.ek.size > 0 && !handler.ek.has(thrown.errorKey)) {
640
+ if (boundaryKey) {
641
+ honeyError = this._createBoundaryError(boundaryKey, thrown);
642
+ } else {
643
+ honeyError = new HoneyError({
644
+ cause: thrown,
645
+ errorKey: EK.internal_server_error,
646
+ status: SK.internal_server_error,
647
+ });
648
+ }
649
+ } else {
650
+ honeyError = thrown;
651
+ }
652
+ } else {
653
+ if (boundaryKey) {
654
+ honeyError = this._createBoundaryError(boundaryKey, thrown);
655
+ } else {
656
+ honeyError = new HoneyError({
657
+ cause: thrown,
658
+ errorKey: EK.internal_server_error,
659
+ status: SK.internal_server_error,
660
+ });
661
+ }
662
+ }
663
+
664
+ await this._resolveI18n(honeyError, ctx, env, request, log);
665
+
666
+ /* onError handler */
667
+ if (this._onError) {
668
+ try {
669
+ const customResult = await this._onError(thrown, this._makeErrorCtx(fc));
670
+ if (customResult instanceof HoneyError) {
671
+ /* user-mapped boundary error — re-run i18n against new errorKey,
672
+ * then fall through to default response path so telemetry +
673
+ * jsonFromError run exactly once. */
674
+ honeyError = customResult;
675
+ await this._resolveI18n(honeyError, ctx, env, request, log);
676
+ } else if (customResult) {
677
+ safeFire(
678
+ () =>
679
+ this._telemetry?.onError?.({
680
+ duration: performance.now() - startTime,
681
+ error: honeyError,
682
+ method,
683
+ path,
684
+ }),
685
+ log,
686
+ );
687
+ safeFire(
688
+ () =>
689
+ this._telemetry?.onResponse?.({
690
+ duration: performance.now() - startTime,
691
+ req: request,
692
+ status: customResult.status,
693
+ }),
694
+ log,
695
+ );
696
+ ctx._isErrorResponse = true;
697
+ return customResult;
698
+ }
699
+ /* customResult === undefined | void → fall through to default path */
700
+ } catch {
701
+ /* swallow onError errors */
702
+ }
703
+ }
704
+
705
+ /* default error response */
706
+ safeFire(
707
+ () =>
708
+ this._telemetry?.onError?.({
709
+ duration: performance.now() - startTime,
710
+ error: honeyError,
711
+ method,
712
+ path,
713
+ }),
714
+ log,
715
+ );
716
+ const res = this._makeErrorCtx(fc).jsonFromError(honeyError);
717
+ safeFire(
718
+ () =>
719
+ this._telemetry?.onResponse?.({
720
+ duration: performance.now() - startTime,
721
+ req: request,
722
+ status: res.status,
723
+ }),
724
+ log,
725
+ );
726
+ ctx._isErrorResponse = true;
727
+ return res;
728
+ }
729
+
730
+ basePath<P extends string>(
731
+ prefix: P,
732
+ ): Honey<
733
+ TEnv,
734
+ TCtx,
735
+ TRoutes,
736
+ TMeta,
737
+ TErrorFactory,
738
+ TDefaultErrors,
739
+ MergePath<TBasePath, P>,
740
+ TTaps,
741
+ TScopedMw
742
+ > {
743
+ const next = new Honey<
744
+ TEnv,
745
+ TCtx,
746
+ TRoutes,
747
+ TMeta,
748
+ TErrorFactory,
749
+ TDefaultErrors,
750
+ MergePath<TBasePath, P>,
751
+ TTaps,
752
+ TScopedMw
753
+ >({
754
+ chainMiddlewares: this._chainMiddlewares,
755
+ defaultErrorKeys: this._defaultErrorKeys,
756
+ globalMiddlewares: this._globalMiddlewares,
757
+ graph: this._graph,
758
+ scopedMiddlewares: this._scopedMiddlewares,
759
+ });
760
+ next._basePath = mergePath(this._basePath, prefix);
761
+ next._defaultBoundaryKey = this._defaultBoundaryKey;
762
+ next._errorFactory = this._errorFactory;
763
+ next._errorSchema = this._errorSchema;
764
+ next._customErrorFormatter = this._customErrorFormatter;
765
+ next._customErrorSchema = this._customErrorSchema;
766
+ next._errorFormatter = this._errorFormatter;
767
+ next._errorI18n = this._errorI18n;
768
+ next._logger = this._logger;
769
+ next._outputValidation = this._outputValidation;
770
+ next._stripPrefix = this._stripPrefix;
771
+ next._trailingSlash = this._trailingSlash;
772
+ next._wsAdapter = this._wsAdapter;
773
+ next._onError = this._onError;
774
+ next._onNotFound = this._onNotFound;
775
+ next._onMethodNotAllowed = this._onMethodNotAllowed;
776
+ next._chainMeta = this._chainMeta;
777
+ next._contextValues = this._contextValues;
778
+ next._taps = this._taps;
779
+ next._telemetry = this._telemetry;
780
+ next._realtimeBus = this._realtimeBus;
781
+ next._realtimeRoutes = this._realtimeRoutes;
782
+ next._staticRoutes = this._staticRoutes;
783
+ next._hasRouteTree = this._hasRouteTree;
784
+ next._hasWsRoutes = this._hasWsRoutes;
785
+ return next as unknown as Honey<
786
+ TEnv,
787
+ TCtx,
788
+ TRoutes,
789
+ TMeta,
790
+ TErrorFactory,
791
+ TDefaultErrors,
792
+ MergePath<TBasePath, P>,
793
+ TTaps,
794
+ TScopedMw
795
+ >;
796
+ }
797
+
798
+ context<TAdds extends Record<string, unknown>>(
799
+ values: TAdds,
800
+ ): Honey<
801
+ TEnv,
802
+ TCtx & Readonly<TAdds>,
803
+ TRoutes,
804
+ TMeta,
805
+ TErrorFactory,
806
+ TDefaultErrors,
807
+ TBasePath,
808
+ TTaps,
809
+ TScopedMw
810
+ > {
811
+ for (const key in values) {
812
+ if (STATIC_CTX_RESERVED.has(key)) {
813
+ throw new Error(`context() cannot set reserved key "${key}"`)
814
+ }
815
+ }
816
+ const next = new Honey<
817
+ TEnv,
818
+ TCtx & Readonly<TAdds>,
819
+ TRoutes,
820
+ TMeta,
821
+ TErrorFactory,
822
+ TDefaultErrors,
823
+ TBasePath,
824
+ TTaps,
825
+ TScopedMw
826
+ >({
827
+ chainMiddlewares: this._chainMiddlewares,
828
+ defaultErrorKeys: this._defaultErrorKeys,
829
+ globalMiddlewares: this._globalMiddlewares,
830
+ graph: this._graph,
831
+ scopedMiddlewares: this._scopedMiddlewares,
832
+ });
833
+ next._basePath = this._basePath;
834
+ next._chainMeta = this._chainMeta;
835
+ next._contextValues = this._contextValues
836
+ ? { ...this._contextValues, ...values }
837
+ : { ...values };
838
+ next._defaultBoundaryKey = this._defaultBoundaryKey;
839
+ next._errorFactory = this._errorFactory;
840
+ next._errorSchema = this._errorSchema;
841
+ next._customErrorFormatter = this._customErrorFormatter;
842
+ next._customErrorSchema = this._customErrorSchema;
843
+ next._errorFormatter = this._errorFormatter;
844
+ next._errorI18n = this._errorI18n;
845
+ next._logger = this._logger;
846
+ next._outputValidation = this._outputValidation;
847
+ next._stripPrefix = this._stripPrefix;
848
+ next._trailingSlash = this._trailingSlash;
849
+ next._wsAdapter = this._wsAdapter;
850
+ next._onError = this._onError;
851
+ next._onNotFound = this._onNotFound;
852
+ next._onMethodNotAllowed = this._onMethodNotAllowed;
853
+ next._taps = this._taps;
854
+ next._telemetry = this._telemetry;
855
+ next._realtimeBus = this._realtimeBus;
856
+ next._realtimeRoutes = this._realtimeRoutes;
857
+ next._staticRoutes = this._staticRoutes;
858
+ next._hasRouteTree = this._hasRouteTree;
859
+ next._hasWsRoutes = this._hasWsRoutes;
860
+ return next as unknown as Honey<TEnv, TCtx & Readonly<TAdds>, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
861
+ }
862
+
863
+ logger(logger: Logger): this {
864
+ this._logger = logger;
865
+ return this;
866
+ }
867
+
868
+ outputValidation(mode: "always" | "dev" | "off"): this {
869
+ this._outputValidation = mode;
870
+ return this;
871
+ }
872
+
873
+ trailingSlash(mode: "enforce" | "ignore" | "strip"): this {
874
+ this._trailingSlash = mode;
875
+ return this;
876
+ }
877
+
878
+ /** Strip a URL path prefix before route matching — boundary-safe (won't strip partial segments), paths without the prefix pass through unchanged */
879
+ stripPrefix(prefix: string): this {
880
+ let normalized = prefix.replace(/\/+$/, "");
881
+ if (normalized.length > 0 && normalized.charCodeAt(0) !== 47) {
882
+ normalized = `/${normalized}`;
883
+ }
884
+ this._stripPrefix = normalized === "" || normalized === "/" ? null : normalized;
885
+ return this;
886
+ }
887
+
888
+ wsAdapter(adapter: WSAdapter): this {
889
+ this._wsAdapter = adapter;
890
+ return this;
891
+ }
892
+
893
+ openapi(options: {
894
+ description?: string
895
+ docs?: "scalar" | "swagger"
896
+ docsPath?: string
897
+ filterRoutes?: (route: { meta: unknown; method: string; path: string }) => boolean
898
+ path?: string
899
+ securitySchemes?: Record<string, unknown>
900
+ title: string
901
+ version: string
902
+ }): this {
903
+ const stem = options.path ?? "/openapi"
904
+ const yamlHeaders = { headers: { "content-type": "application/yaml; charset=utf-8" } }
905
+ const loadJson = (): Promise<unknown> => {
906
+ const epoch = this._root.g ?? 0
907
+ if (this._openApiCache && this._openApiCache.epoch === epoch) {
908
+ return this._openApiCache.value
909
+ }
910
+ let pending: Promise<unknown>
911
+ pending = loadHoneyFeature("openapi")
912
+ .then(() =>
913
+ getOpenApiRuntime().generateOpenApi(this, {
914
+ filterRoutes: options.filterRoutes,
915
+ info: {
916
+ description: options.description,
917
+ title: options.title,
918
+ version: options.version,
919
+ },
920
+ securitySchemes: options.securitySchemes,
921
+ }),
922
+ )
923
+ .catch((err: unknown) => {
924
+ if (this._openApiCache?.value === pending) this._openApiCache = null
925
+ throw err
926
+ })
927
+ this._openApiCache = { epoch, value: pending }
928
+ return pending
929
+ }
930
+ const loadYaml = (): Promise<string> => {
931
+ const epoch = this._root.g ?? 0
932
+ if (this._openApiYamlCache && this._openApiYamlCache.epoch === epoch) {
933
+ return this._openApiYamlCache.value
934
+ }
935
+ let pending: Promise<string>
936
+ pending = Promise.resolve()
937
+ .then(async () => getOpenApiRuntime().toYaml(await loadJson()))
938
+ .catch((err: unknown) => {
939
+ if (this._openApiYamlCache?.value === pending) this._openApiYamlCache = null
940
+ throw err
941
+ })
942
+ this._openApiYamlCache = { epoch, value: pending }
943
+ return pending
944
+ }
945
+ this._mountInternalGet(`${stem}.json`, async (ctx) => ctx.res.json("ok", await loadJson()))
946
+ this._mountInternalGet(`${stem}.yml`, async (ctx) =>
947
+ ctx.res.text("ok", await loadYaml(), yamlHeaders),
948
+ )
949
+ this._mountInternalGet(`${stem}.yaml`, async (ctx) =>
950
+ ctx.res.text("ok", await loadYaml(), yamlHeaders),
951
+ )
952
+ if (options.docs) {
953
+ const specUrl = mergePath(this._basePath, `${stem}.json`)
954
+ const docs = options.docs
955
+ const ui = async (ctx: { res: HoneyRes }) => {
956
+ await loadHoneyFeature("openapi")
957
+ return getOpenApiRuntime().docsUi(docs, specUrl)(ctx)
958
+ }
959
+ const preferred = options.docsPath ?? "/docs"
960
+ const candidates = options.docsPath ? [preferred] : [preferred, "/reference"]
961
+ let mounted = false
962
+ for (const p of candidates) {
963
+ if (this._mountInternalGet(p, ui)) {
964
+ mounted = true
965
+ break
966
+ }
967
+ }
968
+ if (!mounted) {
969
+ throw new Error(
970
+ `Honey.openapi({ docs }) cannot mount at ${preferred}: already registered. Pass docsPath.`,
971
+ )
972
+ }
973
+ }
974
+ return this
975
+ }
976
+
977
+ manifest(options?: { path?: string }): this {
978
+ const path = options?.path ?? "/manifest.json"
979
+ const load = (): Promise<unknown> => {
980
+ const epoch = this._root.g ?? 0
981
+ if (this._manifestCache && this._manifestCache.epoch === epoch) {
982
+ return this._manifestCache.value
983
+ }
984
+ let pending: Promise<unknown>
985
+ pending = loadHoneyFeature("openapi")
986
+ .then(() => getOpenApiRuntime().generateManifest(this))
987
+ .catch((err: unknown) => {
988
+ if (this._manifestCache?.value === pending) this._manifestCache = null
989
+ throw err
990
+ })
991
+ this._manifestCache = { epoch, value: pending }
992
+ return pending
993
+ }
994
+ this._mountInternalGet(path, async (ctx) => ctx.res.json("ok", await load()))
995
+ return this
996
+ }
997
+
998
+ async serve(options?: HoneyServeOptions): Promise<ServeHandle> {
999
+ await loadHoneyFeature("serve")
1000
+ return getServeRuntime()(this, options)
1001
+ }
1002
+
1003
+ private _lookupGet(fullPath: string): RouteHandler | null {
1004
+ const fromStatic = this._staticRoutes.map?.[`GET ${fullPath}`]
1005
+ if (fromStatic) return fromStatic
1006
+ const hit = matchRoute(this._root, "GET", fullPath)
1007
+ if (hit !== null && hit.matched) return hit.handler
1008
+ return null
1009
+ }
1010
+
1011
+ /** Mount an internal GET. Returns false when a user route already owns the path. */
1012
+ private _mountInternalGet(
1013
+ path: string,
1014
+ fn: (ctx: { res: HoneyRes }) => Response | Promise<Response>,
1015
+ ): boolean {
1016
+ const fullPath = mergePath(this._basePath, path)
1017
+ const existing = this._lookupGet(fullPath)
1018
+ if (existing) return existing._skip === true
1019
+ const routeHandler: RouteHandler = {
1020
+ _skip: true,
1021
+ bek: this._defaultBoundaryKey,
1022
+ ef: null,
1023
+ ek: this._defaultErrorKeys,
1024
+ fn: (ctx) => fn(ctx as { res: HoneyRes }),
1025
+ iv: null,
1026
+ mt: null,
1027
+ mw: [...this._chainMiddlewares],
1028
+ os: null,
1029
+ ov: null,
1030
+ rp: fullPath,
1031
+ }
1032
+ insertRoute(this._root, "GET", fullPath, routeHandler)
1033
+ if (!fullPath.includes(":") && !fullPath.includes("*")) {
1034
+ this._registerStatic(`GET ${fullPath}`, routeHandler)
1035
+ }
1036
+ return true
1037
+ }
1038
+
1039
+ defaultErrorFormatter<TSchema extends StandardSchemaLike>(
1040
+ schema: TSchema,
1041
+ fn: (error: HoneyError) => InferOutput<TSchema>,
1042
+ ): this;
1043
+ defaultErrorFormatter(fn: ErrorFormatterFn): this;
1044
+ defaultErrorFormatter(
1045
+ schemaOrFn: ErrorFormatterFn | StandardSchemaLike,
1046
+ maybeFn?: (error: HoneyError) => unknown,
1047
+ ): this {
1048
+ if (typeof schemaOrFn === "function") {
1049
+ this._errorSchema = null;
1050
+ this._errorFormatter = schemaOrFn;
1051
+ } else {
1052
+ this._errorSchema = schemaOrFn;
1053
+ const mapper = maybeFn as (error: HoneyError) => Record<string, unknown>;
1054
+ this._errorFormatter = (error) => mapper(error);
1055
+ }
1056
+ return this;
1057
+ }
1058
+
1059
+ customErrorFormatter<TSchema extends StandardSchemaLike>(
1060
+ schema: TSchema,
1061
+ fn: (error: HoneyError, data: Record<string, unknown>) => InferOutput<TSchema>,
1062
+ ): this;
1063
+ customErrorFormatter(fn: CustomErrorFormatter): this;
1064
+ customErrorFormatter(
1065
+ schemaOrFn: CustomErrorFormatter | StandardSchemaLike,
1066
+ maybeFn?: (error: HoneyError, data: Record<string, unknown>) => unknown,
1067
+ ): this {
1068
+ if (typeof schemaOrFn === "function") {
1069
+ this._customErrorSchema = null;
1070
+ this._customErrorFormatter = schemaOrFn;
1071
+ } else {
1072
+ this._customErrorSchema = schemaOrFn;
1073
+ const mapper = maybeFn as (error: HoneyError, data: Record<string, unknown>) => Record<string, unknown>;
1074
+ this._customErrorFormatter = (error, data) => mapper(error, data);
1075
+ }
1076
+ return this;
1077
+ }
1078
+
1079
+ errorI18n(config: ErrorI18nConfig<TEnv>): this {
1080
+ this._errorI18n = config;
1081
+ return this;
1082
+ }
1083
+
1084
+ onError(
1085
+ handler: (
1086
+ error: unknown,
1087
+ ctx: {
1088
+ env: TEnv;
1089
+ jsonFromError: (err: HoneyError) => Response;
1090
+ req: Request;
1091
+ },
1092
+ ) => HoneyError | Response | Promise<HoneyError | Response | undefined | void> | undefined | void,
1093
+ ): this {
1094
+ this._onError = handler;
1095
+ return this;
1096
+ }
1097
+
1098
+ onMethodNotAllowed(
1099
+ handler: (ctx: {
1100
+ allowed: string[];
1101
+ env: TEnv;
1102
+ jsonFromError: (err: HoneyError) => Response;
1103
+ req: Request;
1104
+ }) => Response | Promise<Response>,
1105
+ ): this {
1106
+ this._onMethodNotAllowed = handler;
1107
+ return this;
1108
+ }
1109
+
1110
+ onNotFound(
1111
+ handler: (ctx: {
1112
+ env: TEnv;
1113
+ jsonFromError: (err: HoneyError) => Response;
1114
+ req: Request;
1115
+ }) => Response | Promise<Response>,
1116
+ ): this {
1117
+ this._onNotFound = handler;
1118
+ return this;
1119
+ }
1120
+
1121
+ /** Register a tap handler keyed by name — fires after successful handler response */
1122
+ tap<K extends string>(
1123
+ key: K,
1124
+ handler: (
1125
+ ctx: TapContext<TEnv>,
1126
+ payload: K extends keyof TTaps ? TTaps[K] : unknown,
1127
+ ) => void | Promise<void>,
1128
+ ): this {
1129
+ if (this._taps === null) {
1130
+ this._taps = new Map();
1131
+ }
1132
+ this._taps.set(key, handler as (ctx: TapContext<TEnv>, payload: unknown) => void | Promise<void>);
1133
+ return this;
1134
+ }
1135
+
1136
+ telemetry(adapter: TelemetryAdapter): this {
1137
+ this._telemetry = adapter;
1138
+ return this;
1139
+ }
1140
+
1141
+ routeTree(tree: RouteTree): this {
1142
+ this._root = tree.root;
1143
+ this._handlerMap = tree.handlers ?? null;
1144
+ this._hasRouteTree = true;
1145
+ return this;
1146
+ }
1147
+
1148
+ toRouteTree(): RouteTree {
1149
+ return { meta: {}, root: this._root };
1150
+ }
1151
+
1152
+ errorFactory<TFactory extends Record<string, (...args: never[]) => unknown>>(
1153
+ factory: TFactory,
1154
+ ): Honey<TEnv, TCtx, TRoutes, TMeta, TFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw> {
1155
+ const next = this as unknown as Honey<
1156
+ TEnv,
1157
+ TCtx,
1158
+ TRoutes,
1159
+ TMeta,
1160
+ TFactory,
1161
+ TDefaultErrors,
1162
+ TBasePath,
1163
+ TTaps,
1164
+ TScopedMw
1165
+ >;
1166
+ next._errorFactory = factory;
1167
+ return next;
1168
+ }
1169
+
1170
+ defaultErrors<
1171
+ TKeys extends [TErrorFactory] extends [never]
1172
+ ? never
1173
+ : keyof TErrorFactory & string,
1174
+ >(
1175
+ ...keys: TKeys[]
1176
+ ): Honey<
1177
+ TEnv,
1178
+ TCtx,
1179
+ TRoutes,
1180
+ TMeta,
1181
+ TErrorFactory,
1182
+ TDefaultErrors | TKeys,
1183
+ TBasePath,
1184
+ TTaps,
1185
+ TScopedMw
1186
+ > {
1187
+ const next = this as unknown as Honey<
1188
+ TEnv,
1189
+ TCtx,
1190
+ TRoutes,
1191
+ TMeta,
1192
+ TErrorFactory,
1193
+ TDefaultErrors | TKeys,
1194
+ TBasePath,
1195
+ TTaps,
1196
+ TScopedMw
1197
+ >;
1198
+ for (const k of keys) {
1199
+ next._defaultErrorKeys.add(k);
1200
+ }
1201
+ return next;
1202
+ }
1203
+
1204
+ defaultBoundary<
1205
+ TKey extends [TErrorFactory] extends [never]
1206
+ ? never
1207
+ : keyof TErrorFactory & string,
1208
+ >(key: TKey): Honey<
1209
+ TEnv,
1210
+ TCtx,
1211
+ TRoutes,
1212
+ TMeta,
1213
+ TErrorFactory,
1214
+ TDefaultErrors | TKey,
1215
+ TBasePath,
1216
+ TTaps,
1217
+ TScopedMw
1218
+ > {
1219
+ const next = this as unknown as Honey<
1220
+ TEnv,
1221
+ TCtx,
1222
+ TRoutes,
1223
+ TMeta,
1224
+ TErrorFactory,
1225
+ TDefaultErrors | TKey,
1226
+ TBasePath,
1227
+ TTaps,
1228
+ TScopedMw
1229
+ >;
1230
+ next._defaultBoundaryKey = key;
1231
+ next._defaultErrorKeys.add(key);
1232
+ return next;
1233
+ }
1234
+
1235
+ /** Phantom overload — constrains what route-level .meta() accepts */
1236
+ meta<TNewMeta extends Record<string, unknown>>(): Honey<
1237
+ TEnv,
1238
+ TCtx,
1239
+ TRoutes,
1240
+ TNewMeta,
1241
+ TErrorFactory,
1242
+ TDefaultErrors,
1243
+ TBasePath,
1244
+ TTaps,
1245
+ TScopedMw
1246
+ >;
1247
+ /** Typed chain-level default meta — constrains route meta and sets runtime defaults in one call */
1248
+ meta<
1249
+ TNewMeta extends Record<string, unknown>,
1250
+ TValues extends Partial<DefaultMeta> & Partial<TNewMeta>,
1251
+ >(
1252
+ values: TValues,
1253
+ ): Honey<
1254
+ TEnv,
1255
+ TCtx,
1256
+ TRoutes,
1257
+ TNewMeta,
1258
+ TErrorFactory,
1259
+ TDefaultErrors,
1260
+ TBasePath,
1261
+ TTaps,
1262
+ TScopedMw
1263
+ >;
1264
+ /** Chain-level default meta — merged into every route registered on this chain */
1265
+ meta<
1266
+ TValues extends Partial<DefaultMeta> &
1267
+ ([TMeta] extends [never] ? {} : Partial<TMeta>),
1268
+ >(
1269
+ values: TValues,
1270
+ ): Honey<
1271
+ TEnv,
1272
+ TCtx,
1273
+ TRoutes,
1274
+ TMeta,
1275
+ TErrorFactory,
1276
+ TDefaultErrors,
1277
+ TBasePath,
1278
+ TTaps,
1279
+ TScopedMw
1280
+ >;
1281
+ meta(values?: Record<string, unknown>): Honey<
1282
+ TEnv,
1283
+ TCtx,
1284
+ TRoutes,
1285
+ unknown,
1286
+ TErrorFactory,
1287
+ TDefaultErrors,
1288
+ TBasePath,
1289
+ TTaps,
1290
+ TScopedMw
1291
+ > {
1292
+ if (values === undefined) {
1293
+ /* phantom overload — type-only, no runtime effect */
1294
+ return this as Honey<TEnv, TCtx, TRoutes, unknown, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
1295
+ }
1296
+ /* chain-level meta — copy-on-write */
1297
+ const next = new Honey<
1298
+ TEnv,
1299
+ TCtx,
1300
+ TRoutes,
1301
+ unknown,
1302
+ TErrorFactory,
1303
+ TDefaultErrors,
1304
+ TBasePath,
1305
+ TTaps,
1306
+ TScopedMw
1307
+ >({
1308
+ chainMiddlewares: this._chainMiddlewares,
1309
+ defaultErrorKeys: this._defaultErrorKeys,
1310
+ globalMiddlewares: this._globalMiddlewares,
1311
+ graph: this._graph,
1312
+ scopedMiddlewares: this._scopedMiddlewares,
1313
+ });
1314
+ next._basePath = this._basePath;
1315
+ next._chainMeta = this._chainMeta
1316
+ ? { ...this._chainMeta, ...values }
1317
+ : { ...values };
1318
+ next._contextValues = this._contextValues;
1319
+ next._defaultBoundaryKey = this._defaultBoundaryKey;
1320
+ next._errorFactory = this._errorFactory;
1321
+ next._errorSchema = this._errorSchema;
1322
+ next._customErrorFormatter = this._customErrorFormatter;
1323
+ next._customErrorSchema = this._customErrorSchema;
1324
+ next._errorFormatter = this._errorFormatter;
1325
+ next._errorI18n = this._errorI18n;
1326
+ next._logger = this._logger;
1327
+ next._outputValidation = this._outputValidation;
1328
+ next._stripPrefix = this._stripPrefix;
1329
+ next._trailingSlash = this._trailingSlash;
1330
+ next._wsAdapter = this._wsAdapter;
1331
+ next._onError = this._onError;
1332
+ next._onNotFound = this._onNotFound;
1333
+ next._onMethodNotAllowed = this._onMethodNotAllowed;
1334
+ next._taps = this._taps;
1335
+ next._telemetry = this._telemetry;
1336
+ next._realtimeBus = this._realtimeBus;
1337
+ next._realtimeRoutes = this._realtimeRoutes;
1338
+ next._staticRoutes = this._staticRoutes;
1339
+ next._hasRouteTree = this._hasRouteTree;
1340
+ next._hasWsRoutes = this._hasWsRoutes;
1341
+ return next;
1342
+ }
1343
+
1344
+ /** Declare tap payload types — auto-extends meta with Partial<T> for meta-driven taps */
1345
+ taps<TNewTaps extends Record<string, unknown>>(): Honey<
1346
+ TEnv,
1347
+ Omit<TCtx, "tap"> & { tap<K extends keyof TNewTaps>(key: K, payload: TNewTaps[K]): void },
1348
+ TRoutes,
1349
+ [TMeta] extends [never] ? Partial<TNewTaps> : TMeta & Partial<TNewTaps>,
1350
+ TErrorFactory,
1351
+ TDefaultErrors,
1352
+ TBasePath,
1353
+ TNewTaps,
1354
+ TScopedMw
1355
+ > {
1356
+ return this as unknown as Honey<
1357
+ TEnv,
1358
+ Omit<TCtx, "tap"> & { tap<K extends keyof TNewTaps>(key: K, payload: TNewTaps[K]): void },
1359
+ TRoutes,
1360
+ [TMeta] extends [never] ? Partial<TNewTaps> : TMeta & Partial<TNewTaps>,
1361
+ TErrorFactory,
1362
+ TDefaultErrors,
1363
+ TBasePath,
1364
+ TNewTaps,
1365
+ TScopedMw
1366
+ >;
1367
+ }
1368
+
1369
+ use<TAdds>(
1370
+ mw: MiddlewareFn<TCtx, TAdds>,
1371
+ ): Honey<TEnv, TCtx & TAdds, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>;
1372
+
1373
+ use<const TPath extends string, TAdds>(
1374
+ path: TPath,
1375
+ mw: MiddlewareFn<TCtx, TAdds>,
1376
+ ): Honey<
1377
+ TEnv,
1378
+ TCtx,
1379
+ TRoutes,
1380
+ TMeta,
1381
+ TErrorFactory,
1382
+ TDefaultErrors,
1383
+ TBasePath,
1384
+ TTaps,
1385
+ readonly [...TScopedMw, { readonly path: MergePath<TBasePath, TPath>; readonly adds: TAdds }]
1386
+ >;
1387
+
1388
+ /* oxlint-disable-next-line typescript/no-explicit-any -- overload impl requires erased types */
1389
+ use(pathOrMw: string | MiddlewareFn<any, any>, maybeMw?: MiddlewareFn<any, any>): any {
1390
+ if (typeof pathOrMw !== "string") {
1391
+ const mw = pathOrMw as RuntimeMiddleware;
1392
+ const newChain = new Honey<TEnv>({
1393
+ chainMiddlewares: [...this._chainMiddlewares, mw],
1394
+ defaultErrorKeys: this._defaultErrorKeys,
1395
+ globalMiddlewares: this._globalMiddlewares,
1396
+ graph: this._graph,
1397
+ scopedMiddlewares: this._scopedMiddlewares,
1398
+ });
1399
+ newChain._basePath = this._basePath;
1400
+ newChain._chainMeta = this._chainMeta;
1401
+ newChain._contextValues = this._contextValues;
1402
+ newChain._defaultBoundaryKey = this._defaultBoundaryKey;
1403
+ newChain._errorFactory = this._errorFactory;
1404
+ newChain._errorSchema = this._errorSchema;
1405
+ newChain._customErrorFormatter = this._customErrorFormatter;
1406
+ newChain._customErrorSchema = this._customErrorSchema;
1407
+ newChain._errorFormatter = this._errorFormatter;
1408
+ newChain._errorI18n = this._errorI18n;
1409
+ newChain._logger = this._logger;
1410
+ newChain._outputValidation = this._outputValidation;
1411
+ newChain._stripPrefix = this._stripPrefix;
1412
+ newChain._trailingSlash = this._trailingSlash;
1413
+ newChain._wsAdapter = this._wsAdapter;
1414
+ newChain._onError = this._onError;
1415
+ newChain._onNotFound = this._onNotFound;
1416
+ newChain._onMethodNotAllowed = this._onMethodNotAllowed;
1417
+ newChain._taps = this._taps;
1418
+ newChain._telemetry = this._telemetry;
1419
+ newChain._realtimeBus = this._realtimeBus;
1420
+ newChain._realtimeRoutes = this._realtimeRoutes;
1421
+ newChain._staticRoutes = this._staticRoutes;
1422
+ newChain._hasRouteTree = this._hasRouteTree;
1423
+ newChain._hasWsRoutes = this._hasWsRoutes;
1424
+ return newChain;
1425
+ }
1426
+
1427
+ /* scoped path */
1428
+ const normalizedPrefix = normalizeScopePath(pathOrMw);
1429
+ const fullPrefix = mergePath(this._basePath, normalizedPrefix);
1430
+ const mw = maybeMw as RuntimeMiddleware;
1431
+ const mwWithErrors = maybeMw as MiddlewareFn<unknown, unknown>;
1432
+ const entry: ScopedEntry = {
1433
+ errors: mwWithErrors.errors ? [...mwWithErrors.errors] : undefined,
1434
+ mw,
1435
+ prefix: fullPrefix,
1436
+ };
1437
+ this._scopedMiddlewares.push(entry);
1438
+ const newChain = new Honey<TEnv>({
1439
+ chainMiddlewares: this._chainMiddlewares,
1440
+ defaultErrorKeys: this._defaultErrorKeys,
1441
+ globalMiddlewares: this._globalMiddlewares,
1442
+ graph: this._graph,
1443
+ scopedMiddlewares: this._scopedMiddlewares,
1444
+ });
1445
+ newChain._basePath = this._basePath;
1446
+ newChain._chainMeta = this._chainMeta;
1447
+ newChain._contextValues = this._contextValues;
1448
+ newChain._defaultBoundaryKey = this._defaultBoundaryKey;
1449
+ newChain._errorFactory = this._errorFactory;
1450
+ newChain._errorSchema = this._errorSchema;
1451
+ newChain._customErrorFormatter = this._customErrorFormatter;
1452
+ newChain._customErrorSchema = this._customErrorSchema;
1453
+ newChain._errorFormatter = this._errorFormatter;
1454
+ newChain._errorI18n = this._errorI18n;
1455
+ newChain._logger = this._logger;
1456
+ newChain._outputValidation = this._outputValidation;
1457
+ newChain._stripPrefix = this._stripPrefix;
1458
+ newChain._trailingSlash = this._trailingSlash;
1459
+ newChain._wsAdapter = this._wsAdapter;
1460
+ newChain._onError = this._onError;
1461
+ newChain._onNotFound = this._onNotFound;
1462
+ newChain._onMethodNotAllowed = this._onMethodNotAllowed;
1463
+ newChain._taps = this._taps;
1464
+ newChain._telemetry = this._telemetry;
1465
+ newChain._realtimeBus = this._realtimeBus;
1466
+ newChain._realtimeRoutes = this._realtimeRoutes;
1467
+ newChain._staticRoutes = this._staticRoutes;
1468
+ newChain._hasRouteTree = this._hasRouteTree;
1469
+ newChain._hasWsRoutes = this._hasWsRoutes;
1470
+ newChain._applyScopedEntryErrors(entry);
1471
+ return newChain;
1472
+ }
1473
+
1474
+ route<
1475
+ TSubRoutes,
1476
+ TSubMeta,
1477
+ TSubErrorFactory,
1478
+ TSubDefaultErrors extends string,
1479
+ TSubBasePath extends string,
1480
+ >(
1481
+ sub: Honey<
1482
+ TEnv,
1483
+ TCtx,
1484
+ TSubRoutes,
1485
+ TSubMeta,
1486
+ TSubErrorFactory,
1487
+ TSubDefaultErrors,
1488
+ TSubBasePath
1489
+ >,
1490
+ ): Honey<
1491
+ TEnv,
1492
+ TCtx,
1493
+ TRoutes & TSubRoutes,
1494
+ TMeta,
1495
+ TErrorFactory,
1496
+ TDefaultErrors,
1497
+ TBasePath,
1498
+ TTaps,
1499
+ TScopedMw
1500
+ > {
1501
+ /* skip self-merge: .handler() already inserted into shared _root */
1502
+ if (sub._tree !== this._root) {
1503
+ mergeInto(this._root, sub._tree);
1504
+ if (sub._hasWsRoutes) this._hasWsRoutes = true;
1505
+ if (sub._hasRouteTree) this._hasRouteTree = true;
1506
+ /* carry sub's scoped mw entries into parent's runtime list (parent scopes run first) */
1507
+ for (const entry of sub._scopedMiddlewares) {
1508
+ this._scopedMiddlewares.push(entry);
1509
+ }
1510
+ if (sub._staticRoutes.map !== null) {
1511
+ if (this._staticRoutes.map === null) {
1512
+ this._staticRoutes.map = Object.create(null) as Record<string, RouteHandler>;
1513
+ }
1514
+ Object.assign(this._staticRoutes.map, sub._staticRoutes.map);
1515
+ }
1516
+ for (const [path, cfg] of sub._realtimeRoutes) {
1517
+ if (this._realtimeRoutes.has(path)) {
1518
+ throw new Error(`Duplicate realtime route: ${path}`);
1519
+ }
1520
+ this._realtimeRoutes.set(path, cfg);
1521
+ }
1522
+ if (!this._realtimeBus && sub._realtimeBus) {
1523
+ this._realtimeBus = sub._realtimeBus;
1524
+ }
1525
+ if (sub._taps !== null) {
1526
+ if (this._taps === null) this._taps = new Map();
1527
+ for (const [key, fn] of sub._taps) {
1528
+ if (!this._taps.has(key)) this._taps.set(key, fn);
1529
+ }
1530
+ }
1531
+ /* walk tree and apply all scoped error keys to matching handlers */
1532
+ this._applyAllScopedErrors();
1533
+ }
1534
+ return this as unknown as Honey<
1535
+ TEnv,
1536
+ TCtx,
1537
+ TRoutes & TSubRoutes,
1538
+ TMeta,
1539
+ TErrorFactory,
1540
+ TDefaultErrors,
1541
+ TBasePath,
1542
+ TTaps,
1543
+ TScopedMw
1544
+ >;
1545
+ }
1546
+
1547
+ private _registerRoute<
1548
+ TPath extends string,
1549
+ TMethod extends HttpMethod | "ALL",
1550
+ >(
1551
+ method: TMethod,
1552
+ path: TPath,
1553
+ extraMethods?: (HttpMethod | "ALL")[],
1554
+ ): BuilderChain<
1555
+ TEnv,
1556
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1557
+ {},
1558
+ never,
1559
+ {},
1560
+ MergePath<TBasePath, TPath>,
1561
+ TMethod,
1562
+ TRoutes,
1563
+ never,
1564
+ TCtx,
1565
+ TMeta,
1566
+ InitAccMeta<TMeta>,
1567
+ TErrorFactory,
1568
+ TDefaultErrors,
1569
+ TBasePath,
1570
+ TTaps,
1571
+ TScopedMw
1572
+ > {
1573
+ const fullPath = mergePath(this._basePath, path);
1574
+ const errorKeys = new Set(this._defaultErrorKeys);
1575
+ for (const entry of this._scopedMiddlewares) {
1576
+ if (entry.errors && scopeMatches(entry.prefix, fullPath)) {
1577
+ for (const k of entry.errors) errorKeys.add(k);
1578
+ }
1579
+ }
1580
+ return new RouteBuilder<
1581
+ TEnv,
1582
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1583
+ {},
1584
+ never,
1585
+ {},
1586
+ MergePath<TBasePath, TPath>,
1587
+ TMethod,
1588
+ TRoutes,
1589
+ never,
1590
+ TCtx,
1591
+ TMeta,
1592
+ InitAccMeta<TMeta>,
1593
+ TErrorFactory,
1594
+ TDefaultErrors,
1595
+ TBasePath,
1596
+ TTaps,
1597
+ TScopedMw
1598
+ >({
1599
+ boundaryErrorKey: this._defaultBoundaryKey,
1600
+ errorKeys,
1601
+ extraMethods: extraMethods ?? null,
1602
+ handlerMap: this._handlerMap,
1603
+ inputSchemas: null,
1604
+ meta: this._chainMeta ? { ...this._chainMeta } : null,
1605
+ method,
1606
+ middlewares: [],
1607
+ outputSchemas: null,
1608
+ parent: this,
1609
+ parentMiddlewares: this._chainMiddlewares,
1610
+ path: fullPath,
1611
+ root: this._root,
1612
+ }) as unknown as BuilderChain<
1613
+ TEnv,
1614
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1615
+ {},
1616
+ never,
1617
+ {},
1618
+ MergePath<TBasePath, TPath>,
1619
+ TMethod,
1620
+ TRoutes,
1621
+ never,
1622
+ TCtx,
1623
+ TMeta,
1624
+ InitAccMeta<TMeta>,
1625
+ TErrorFactory,
1626
+ TDefaultErrors,
1627
+ TBasePath,
1628
+ TTaps,
1629
+ TScopedMw
1630
+ >;
1631
+ }
1632
+
1633
+ on<
1634
+ const TPath extends string,
1635
+ const TMethods extends readonly [HttpMethod | "ALL", ...(HttpMethod | "ALL")[]],
1636
+ >(
1637
+ methods: TMethods,
1638
+ path: TPath,
1639
+ ): BuilderChain<
1640
+ TEnv,
1641
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1642
+ {},
1643
+ never,
1644
+ {},
1645
+ MergePath<TBasePath, TPath>,
1646
+ TMethods[number],
1647
+ TRoutes,
1648
+ never,
1649
+ TCtx,
1650
+ TMeta,
1651
+ InitAccMeta<TMeta>,
1652
+ TErrorFactory,
1653
+ TDefaultErrors,
1654
+ TBasePath,
1655
+ TTaps,
1656
+ TScopedMw
1657
+ > {
1658
+ const [first, ...rest] = methods;
1659
+ return this._registerRoute<TPath, TMethods[number]>(
1660
+ first,
1661
+ path,
1662
+ rest.length > 0 ? (rest as (HttpMethod | "ALL")[]) : undefined,
1663
+ );
1664
+ }
1665
+
1666
+ all<const TPath extends string>(path: TPath) {
1667
+ return this._registerRoute<TPath, "ALL">("ALL", path);
1668
+ }
1669
+ delete<const TPath extends string>(path: TPath) {
1670
+ return this._registerRoute<TPath, "DELETE">("DELETE", path);
1671
+ }
1672
+ get<const TPath extends string>(path: TPath) {
1673
+ return this._registerRoute<TPath, "GET">("GET", path);
1674
+ }
1675
+ head<const TPath extends string>(path: TPath) {
1676
+ return this._registerRoute<TPath, "HEAD">("HEAD", path);
1677
+ }
1678
+ options<const TPath extends string>(path: TPath) {
1679
+ return this._registerRoute<TPath, "OPTIONS">("OPTIONS", path);
1680
+ }
1681
+ patch<const TPath extends string>(path: TPath) {
1682
+ return this._registerRoute<TPath, "PATCH">("PATCH", path);
1683
+ }
1684
+ post<const TPath extends string>(path: TPath) {
1685
+ return this._registerRoute<TPath, "POST">("POST", path);
1686
+ }
1687
+ put<const TPath extends string>(path: TPath) {
1688
+ return this._registerRoute<TPath, "PUT">("PUT", path);
1689
+ }
1690
+
1691
+ ws<const TPath extends string>(
1692
+ path: TPath,
1693
+ ): WSRouteBuilder<
1694
+ TEnv,
1695
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1696
+ {},
1697
+ never,
1698
+ Honey<TEnv, TCtx, TRoutes, TMeta, TErrorFactory, TDefaultErrors, TBasePath, TTaps, TScopedMw>
1699
+ > {
1700
+ return new WSRouteBuilder<
1701
+ TEnv,
1702
+ TCtx & ApplyScoped<TScopedMw, MergePath<TBasePath, TPath>>,
1703
+ {},
1704
+ never,
1705
+ Honey<
1706
+ TEnv,
1707
+ TCtx,
1708
+ TRoutes,
1709
+ TMeta,
1710
+ TErrorFactory,
1711
+ TDefaultErrors,
1712
+ TBasePath,
1713
+ TTaps,
1714
+ TScopedMw
1715
+ >
1716
+ >({
1717
+ boundaryErrorKey: this._defaultBoundaryKey,
1718
+ errorKeys: new Set(),
1719
+ inputSchemas: null,
1720
+ meta: null,
1721
+ middlewares: [],
1722
+ parent: this,
1723
+ parentMiddlewares: this._chainMiddlewares,
1724
+ path: mergePath(this._basePath, path),
1725
+ root: this._root,
1726
+ });
1727
+ }
1728
+
1729
+ realtime(path: string, opts: RealtimeRouteOpts): this {
1730
+ const fullPath = mergePath(this._basePath, path) || "/";
1731
+
1732
+ if (!this._realtimeBus) {
1733
+ this._realtimeBus = createBus();
1734
+ }
1735
+
1736
+ if (this._realtimeRoutes.has(fullPath)) {
1737
+ throw new Error(`Duplicate realtime route: ${fullPath}`);
1738
+ }
1739
+
1740
+ this._realtimeRoutes.set(fullPath, {
1741
+ handler: opts.handler,
1742
+ middlewares: opts.use,
1743
+ reconnectBuffer: opts.reconnectBuffer,
1744
+ });
1745
+
1746
+ const mw: RuntimeMiddleware[] = [];
1747
+ if (opts.use) {
1748
+ for (const fn of opts.use) {
1749
+ mw.push(fn as RuntimeMiddleware);
1750
+ }
1751
+ }
1752
+
1753
+ insertWsRoute(this._root, fullPath, {
1754
+ bek: this._defaultBoundaryKey,
1755
+ ek: new Set(),
1756
+ fn: Object.create(null),
1757
+ iv: null,
1758
+ mt: null,
1759
+ mw: [...this._chainMiddlewares, ...mw],
1760
+ rp: fullPath,
1761
+ });
1762
+
1763
+ this._hasWsRoutes = true;
1764
+ return this;
1765
+ }
1766
+
1767
+ /**
1768
+ * Deno must call `Deno.upgradeWebSocket` in the same turn as the serve
1769
+ * callback. When the adapter implements `preUpgrade`, we do that here
1770
+ * and return the 101 response before any middleware Promise.
1771
+ */
1772
+ fetch(
1773
+ request: Request,
1774
+ env: TEnv,
1775
+ executionCtx?: { waitUntil?: (p: Promise<unknown>) => void },
1776
+ ): Response | Promise<Response> {
1777
+ if (this._wsAdapter === null && !this._hasWsRoutes) {
1778
+ return this._doFetch(request, env, executionCtx);
1779
+ }
1780
+ const isWsUpgrade = request.headers.get("upgrade")?.toLowerCase() === "websocket"
1781
+ const path = this.pathFromRequest(request)
1782
+ const canPreUpgrade =
1783
+ isWsUpgrade &&
1784
+ this._wsAdapter?.preUpgrade !== undefined &&
1785
+ !this.trailingSlashRedirects(path) &&
1786
+ matchWsRoute(this._root, this.pathAfterPrefix(path)) !== null
1787
+ const pre = canPreUpgrade ? this._wsAdapter?.preUpgrade?.(request) : undefined
1788
+ const work = this._doFetch(request, env, executionCtx)
1789
+ if (!pre) return work
1790
+ void Promise.resolve(work)
1791
+ .then((res) => {
1792
+ if (res.status === 101) return
1793
+ const code = res.status === 401 ? 4401 : 1008
1794
+ const reason = res.status === 401 ? "unauthorized" : "upgrade rejected"
1795
+ const reject = () => {
1796
+ try {
1797
+ pre.socket.close(code, reason)
1798
+ } catch {
1799
+ /* already closed */
1800
+ }
1801
+ }
1802
+ if (pre.whenOpen) pre.whenOpen(reject)
1803
+ else reject()
1804
+ })
1805
+ .catch(() => {
1806
+ try {
1807
+ pre.socket.close(1011, "internal error")
1808
+ } catch {
1809
+ /* already closed */
1810
+ }
1811
+ })
1812
+ return pre.response
1813
+ }
1814
+
1815
+ private pathFromRequest(request: Request): string {
1816
+ const rawUrl = request.url
1817
+ const protoEnd = rawUrl.indexOf("//")
1818
+ const pathStart = protoEnd === -1 ? 0 : rawUrl.indexOf("/", protoEnd + 2)
1819
+ const searchOrHash = pathStart === -1 ? -1 : findSearchOrHash(rawUrl, pathStart)
1820
+ if (pathStart === -1) return "/"
1821
+ if (searchOrHash === -1) return rawUrl.substring(pathStart)
1822
+ return rawUrl.substring(pathStart, searchOrHash)
1823
+ }
1824
+
1825
+ private trailingSlashRedirects(path: string): boolean {
1826
+ if (path.length <= 1) return false
1827
+ if (this._trailingSlash === "strip" && path.endsWith("/")) return true
1828
+ if (this._trailingSlash === "enforce" && !path.endsWith("/")) return true
1829
+ return false
1830
+ }
1831
+
1832
+ private pathAfterPrefix(path: string): string {
1833
+ if (this._stripPrefix === null) return path
1834
+ if (path === this._stripPrefix) return "/"
1835
+ if (path.startsWith(this._stripPrefix) && path.charCodeAt(this._stripPrefix.length) === 47) {
1836
+ return path.slice(this._stripPrefix.length)
1837
+ }
1838
+ return path
1839
+ }
1840
+
1841
+ private _doFetch(
1842
+ request: Request,
1843
+ env: TEnv,
1844
+ executionCtx?: { waitUntil?: (p: Promise<unknown>) => void },
1845
+ ): Response | Promise<Response> {
1846
+ const startTime = performance.now();
1847
+
1848
+ /* fast path extraction — avoids expensive new URL() allocation */
1849
+ const rawUrl = request.url;
1850
+ const protoEnd = rawUrl.indexOf("//");
1851
+ const pathStart = protoEnd === -1 ? 0 : rawUrl.indexOf("/", protoEnd + 2);
1852
+ const searchOrHash =
1853
+ pathStart === -1 ? -1 : findSearchOrHash(rawUrl, pathStart);
1854
+ let path: string;
1855
+ if (pathStart === -1) {
1856
+ path = "/";
1857
+ } else if (searchOrHash === -1) {
1858
+ path = rawUrl.substring(pathStart);
1859
+ } else {
1860
+ path = rawUrl.substring(pathStart, searchOrHash);
1861
+ }
1862
+
1863
+ /* lazily create URL only when actually needed (search params, redirects) */
1864
+ let _url: URL | undefined;
1865
+ const getUrl = (): URL => {
1866
+ if (_url === undefined) _url = new URL(rawUrl);
1867
+ return _url;
1868
+ };
1869
+
1870
+ const log = this._logger ?? undefined;
1871
+ const fc: FetchCtx<TEnv> = {
1872
+ env,
1873
+ executionCtx,
1874
+ log,
1875
+ request,
1876
+ startTime,
1877
+ url: getUrl,
1878
+ };
1879
+
1880
+ if (this._telemetry !== null) {
1881
+ safeFire(() => this._telemetry?.onRequest?.({ env, req: request }), log);
1882
+ }
1883
+
1884
+ /* trailing slash handling */
1885
+ if (path.length > 1) {
1886
+ if (this._trailingSlash === "strip" && path.endsWith("/")) {
1887
+ const redirectUrl = getUrl();
1888
+ redirectUrl.pathname = path.slice(0, -1);
1889
+ return new Response(null, {
1890
+ headers: { location: redirectUrl.toString() },
1891
+ status: 308,
1892
+ });
1893
+ }
1894
+ if (this._trailingSlash === "enforce" && !path.endsWith("/")) {
1895
+ const redirectUrl = getUrl();
1896
+ redirectUrl.pathname = `${path}/`;
1897
+ return new Response(null, {
1898
+ headers: { location: redirectUrl.toString() },
1899
+ status: 308,
1900
+ });
1901
+ }
1902
+ }
1903
+
1904
+ /* prefix stripping — must run AFTER trailing slash so redirects preserve the full prefixed URL */
1905
+ if (this._stripPrefix !== null) {
1906
+ if (path === this._stripPrefix) {
1907
+ path = "/";
1908
+ } else if (
1909
+ path.startsWith(this._stripPrefix) &&
1910
+ path.charCodeAt(this._stripPrefix.length) === 47
1911
+ ) {
1912
+ path = path.slice(this._stripPrefix.length);
1913
+ }
1914
+ }
1915
+
1916
+ /* WebSocket route check */
1917
+ const isWsUpgrade = request.headers.get("upgrade") === "websocket";
1918
+ if (isWsUpgrade) {
1919
+ const wsMatch = matchWsRoute(this._root, path);
1920
+ if (wsMatch !== null) {
1921
+ return this._handleWs(fc, wsMatch);
1922
+ }
1923
+ } else if (this._realtimeRoutes.size > 0) {
1924
+ /* Non-upgrade request hitting a realtime-only path → 426 Upgrade Required */
1925
+ const wsMatch = matchWsRoute(this._root, path);
1926
+ if (wsMatch !== null && this._realtimeRoutes.has(wsMatch.handler.rp)) {
1927
+ return new Response(null, { headers: { upgrade: "websocket" }, status: 426 });
1928
+ }
1929
+ }
1930
+
1931
+ const method = request.method.toUpperCase() as HttpMethod;
1932
+
1933
+ /* fn:null fallthrough — tree match sets meta, catch-all dispatches */
1934
+ let fnNullMeta: Record<string, unknown> | null = null;
1935
+ let fnNullParams: Record<string, string> | null = null;
1936
+ let fnNullHit = false;
1937
+
1938
+ /* Tier 2: O(1) static route lookup — checks both precompiled and runtime maps */
1939
+ const smap = this._staticRoutes.map ?? this._handlerMap;
1940
+ if (smap !== null) {
1941
+ const key = `${method} ${path}`;
1942
+ const staticHandler = smap[key];
1943
+ if (staticHandler) {
1944
+ if (staticHandler.fn === null) {
1945
+ fnNullMeta = staticHandler.mt;
1946
+ fnNullParams = EMPTY_PARAMS;
1947
+ fnNullHit = true;
1948
+ } else {
1949
+ return this._handleMatched(
1950
+ fc,
1951
+ method,
1952
+ path,
1953
+ staticHandler,
1954
+ EMPTY_PARAMS,
1955
+ );
1956
+ }
1957
+ }
1958
+ /* HEAD falls back to GET */
1959
+ if (!fnNullHit && method === "HEAD") {
1960
+ const getHandler = smap[`GET ${path}`];
1961
+ if (getHandler) {
1962
+ if (getHandler.fn === null) {
1963
+ fnNullMeta = getHandler.mt;
1964
+ fnNullParams = EMPTY_PARAMS;
1965
+ fnNullHit = true;
1966
+ } else {
1967
+ return this._handleMatched(
1968
+ fc,
1969
+ method,
1970
+ path,
1971
+ getHandler,
1972
+ EMPTY_PARAMS,
1973
+ );
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+
1979
+ if (!fnNullHit) {
1980
+ const result = matchRoute(this._root, method, path);
1981
+
1982
+ if (result?.matched) {
1983
+ /*
1984
+ * routeTree loaded → wildcard matches for unknown paths must 404.
1985
+ * Only fn:null fallthroughs (known routes) should reach the catch-all.
1986
+ * Detect wildcard: handler has no mt and fn is NOT null (builder-registered catch-all).
1987
+ */
1988
+ const isWildcardCatchAll =
1989
+ this._hasRouteTree &&
1990
+ this._root.w !== null &&
1991
+ result.handler.fn !== null &&
1992
+ (this._root.w.m[method] === result.handler ||
1993
+ this._root.w.m["ALL"] === result.handler);
1994
+
1995
+ if (isWildcardCatchAll) {
1996
+ /* path not in routeTree — 404 */
1997
+ return this._handle404(fc, method, path);
1998
+ }
1999
+
2000
+ if (result.handler.fn === null) {
2001
+ fnNullMeta = result.handler.mt;
2002
+ fnNullParams = result.params;
2003
+ fnNullHit = true;
2004
+ } else {
2005
+ return this._handleMatched(
2006
+ fc,
2007
+ method,
2008
+ path,
2009
+ result.handler,
2010
+ result.params,
2011
+ );
2012
+ }
2013
+ }
2014
+
2015
+ if (!fnNullHit) {
2016
+ if (result === null) {
2017
+ if (!isWsUpgrade) {
2018
+ const wsMatch = matchWsRoute(this._root, path);
2019
+ if (wsMatch !== null) {
2020
+ return new Response("Upgrade Required", {
2021
+ headers: { connection: "Upgrade", upgrade: "websocket" },
2022
+ status: 426,
2023
+ });
2024
+ }
2025
+ }
2026
+ return this._handle404(fc, method, path);
2027
+ }
2028
+
2029
+ if (!result.matched) {
2030
+ if (
2031
+ method === "OPTIONS" &&
2032
+ fc.request.headers.has("access-control-request-method") &&
2033
+ result.allowed.length > 0
2034
+ ) {
2035
+ const fallback = this._preflightFallbackMethod(result.allowed);
2036
+ const retry = matchRoute(this._root, fallback, path);
2037
+ if (retry?.matched && retry.handler.fn !== null) {
2038
+ return this._handleCorsPreflight(
2039
+ fc,
2040
+ path,
2041
+ retry.handler,
2042
+ retry.params,
2043
+ result.allowed,
2044
+ );
2045
+ }
2046
+ }
2047
+ return this._handle405(fc, method, path, result.allowed);
2048
+ }
2049
+ }
2050
+ }
2051
+
2052
+ /* fn:null hit — find catch-all/wildcard to dispatch with stashed meta */
2053
+ const wildcardResult = matchRoute(this._root, method, "/*");
2054
+ if (wildcardResult?.matched && wildcardResult.handler.fn !== null) {
2055
+ return this._handleMatched(
2056
+ fc,
2057
+ method,
2058
+ path,
2059
+ wildcardResult.handler,
2060
+ fnNullParams ?? EMPTY_PARAMS,
2061
+ fnNullMeta,
2062
+ );
2063
+ }
2064
+
2065
+ return this._handle404(fc, method, path);
2066
+ }
2067
+
2068
+ private _makeErrorCtx(fc: FetchCtx<TEnv>, allowed?: string[]) {
2069
+ return {
2070
+ env: fc.env,
2071
+ jsonFromError: (err: HoneyError) =>
2072
+ createErrorResponse(err, this._errorFormatter, this._customErrorFormatter),
2073
+ req: fc.request,
2074
+ ...(allowed ? { allowed } : {}),
2075
+ };
2076
+ }
2077
+
2078
+ private async _handleWs(
2079
+ fc: FetchCtx<TEnv>,
2080
+ wsMatch: { handler: WSRouteHandler; params: Record<string, string> },
2081
+ ): Promise<Response> {
2082
+ /* Dispatch to realtime handler if this path is registered as a realtime route */
2083
+ const realtimeConfig = this._realtimeRoutes.get(wsMatch.handler.rp);
2084
+ if (realtimeConfig) {
2085
+ return this._handleRealtime(fc, wsMatch, realtimeConfig);
2086
+ }
2087
+
2088
+ const isUpgrade =
2089
+ fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
2090
+ if (!isUpgrade) {
2091
+ return new Response(null, {
2092
+ headers: { upgrade: "websocket" },
2093
+ status: 426,
2094
+ });
2095
+ }
2096
+
2097
+ const wsAdapter = this._wsAdapter;
2098
+ if (!wsAdapter) {
2099
+ fc.log?.warn?.("WebSocket adapter not configured — call .wsAdapter()");
2100
+ return createErrorResponse(
2101
+ this._createError(EK.internal_server_error, SK.internal_server_error),
2102
+ this._errorFormatter,
2103
+ this._customErrorFormatter,
2104
+ );
2105
+ }
2106
+
2107
+ const wsCtx = new HoneyContext({
2108
+ env: fc.env,
2109
+ executionCtx: fc.executionCtx,
2110
+ params: wsMatch.params,
2111
+ req: fc.request,
2112
+ urlFn: fc.url,
2113
+ });
2114
+ if (this._contextValues) Object.assign(wsCtx, this._contextValues);
2115
+
2116
+ /*
2117
+ * WS ordering: [global → scoped → chain+handler-route-specific]
2118
+ * WS bakes chain mw into handler.mw at registration, so scoped runs before chain.
2119
+ * This is an unavoidable inconsistency vs HTTP (where chain runs before scoped).
2120
+ */
2121
+ const scopedForPath = this._filterScopedForPath(wsMatch.handler.rp);
2122
+ const allWsMw: RuntimeMiddleware[] = [
2123
+ ...this._globalMiddlewares,
2124
+ ...scopedForPath,
2125
+ ...wsMatch.handler.mw,
2126
+ ];
2127
+
2128
+ try {
2129
+ return await executeChain(allWsMw, wsCtx, async (finalCtx) => {
2130
+ const userHandler = wsMatch.handler.fn;
2131
+ let messageQueue: Promise<void> = Promise.resolve();
2132
+
2133
+ const onOpenFn = userHandler.onOpen;
2134
+ const onMsgFn = userHandler.onMessage;
2135
+ const onCloseFn = userHandler.onClose;
2136
+ const onErrorFn = userHandler.onError;
2137
+ const onReconnectFn = userHandler.onReconnect;
2138
+ const reconnectToken = fc.url().searchParams.get("reconnect_token");
2139
+
2140
+ const wrappedHandler: WSHandler<unknown> = {};
2141
+
2142
+ if (reconnectToken && onReconnectFn) {
2143
+ wrappedHandler.onOpen = (_ctx, ws) => {
2144
+ onReconnectFn(finalCtx, ws, reconnectToken);
2145
+ };
2146
+ } else if (onOpenFn) {
2147
+ wrappedHandler.onOpen = (_ctx, ws) => {
2148
+ onOpenFn(finalCtx, ws);
2149
+ };
2150
+ }
2151
+
2152
+ if (onMsgFn) {
2153
+ wrappedHandler.onMessage = (_ctx, ws, data) => {
2154
+ messageQueue = messageQueue
2155
+ .then(() => onMsgFn(finalCtx, ws, data))
2156
+ .catch((err: unknown) => {
2157
+ onErrorFn?.(finalCtx, ws, err);
2158
+ });
2159
+ };
2160
+ }
2161
+
2162
+ if (onCloseFn) {
2163
+ wrappedHandler.onClose = (_ctx, ws, code, reason) => {
2164
+ onCloseFn(finalCtx, ws, code, reason);
2165
+ };
2166
+ }
2167
+
2168
+ if (onErrorFn) {
2169
+ wrappedHandler.onError = (_ctx, ws, error) => {
2170
+ onErrorFn(finalCtx, ws, error);
2171
+ };
2172
+ }
2173
+
2174
+ const upgradeResult = await wsAdapter.upgrade(
2175
+ fc.request,
2176
+ fc.env,
2177
+ wrappedHandler,
2178
+ );
2179
+ return upgradeResult.response;
2180
+ });
2181
+ } catch (thrown) {
2182
+ return this._toErrorResponse(thrown);
2183
+ }
2184
+ }
2185
+
2186
+ private async _handleRealtime(
2187
+ fc: FetchCtx<TEnv>,
2188
+ wsMatch: { handler: WSRouteHandler; params: Record<string, string> },
2189
+ config: { handler: RealtimeRouteOpts["handler"]; middlewares?: RealtimeRouteOpts["use"]; reconnectBuffer?: number },
2190
+ ): Promise<Response> {
2191
+ const isUpgrade =
2192
+ fc.request.headers.get("upgrade")?.toLowerCase() === "websocket";
2193
+ if (!isUpgrade) {
2194
+ return new Response(null, {
2195
+ headers: { upgrade: "websocket" },
2196
+ status: 426,
2197
+ });
2198
+ }
2199
+
2200
+ const wsAdapter = this._wsAdapter;
2201
+ if (!wsAdapter) {
2202
+ fc.log?.warn?.("WebSocket adapter not configured — call .wsAdapter()");
2203
+ return createErrorResponse(
2204
+ this._createError(EK.internal_server_error, SK.internal_server_error),
2205
+ this._errorFormatter,
2206
+ this._customErrorFormatter,
2207
+ );
2208
+ }
2209
+
2210
+ /* Lazily create bus if not yet initialized (happens when realtime() was called on a child chain) */
2211
+ if (!this._realtimeBus) {
2212
+ this._realtimeBus = createBus();
2213
+ }
2214
+ const bus = this._realtimeBus;
2215
+
2216
+ const ctx = new HoneyContext({
2217
+ env: fc.env,
2218
+ executionCtx: fc.executionCtx,
2219
+ params: wsMatch.params,
2220
+ req: fc.request,
2221
+ urlFn: fc.url,
2222
+ });
2223
+ if (this._contextValues) Object.assign(ctx, this._contextValues);
2224
+ Object.assign(ctx, {
2225
+ realtime: { publish: (topic: string, data: unknown) => bus.publish(topic, data) },
2226
+ });
2227
+
2228
+ const scopedForPath = this._filterScopedForPath(wsMatch.handler.rp);
2229
+ const allMw: RuntimeMiddleware[] = [
2230
+ ...this._globalMiddlewares,
2231
+ ...scopedForPath,
2232
+ ...wsMatch.handler.mw,
2233
+ ];
2234
+
2235
+ try {
2236
+ return await executeChain(allMw, ctx, async (finalCtx) => {
2237
+ const connId = crypto.randomUUID();
2238
+
2239
+ let socket: WSContext<unknown> | null = null;
2240
+ let conn: ReturnType<typeof createConnContext> | null = null;
2241
+
2242
+ /*
2243
+ * initConn creates the ConnContext and calls the user handler.
2244
+ * Called from onOpen (for Bun where socket arrives later)
2245
+ * or inline after upgrade (for Node/CF where socket is immediate).
2246
+ */
2247
+ const initConn = (ws: WSContext<unknown>) => {
2248
+ socket = ws;
2249
+ conn = createConnContext({
2250
+ bus,
2251
+ closeFn: (reason) => {
2252
+ if (socket) socket.close(1000, reason);
2253
+ },
2254
+ id: connId,
2255
+ sendFn: (payload) => {
2256
+ if (socket) socket.send(typeof payload === "object" && payload !== null ? JSON.stringify(payload) : String(payload));
2257
+ },
2258
+ transport: "ws",
2259
+ userId: null,
2260
+ });
2261
+
2262
+ bus.onMessage(connId, (data) => {
2263
+ if (socket) {
2264
+ socket.send(typeof data === "object" && data !== null ? JSON.stringify(data) : String(data));
2265
+ }
2266
+ });
2267
+
2268
+ config.handler(finalCtx, conn);
2269
+ };
2270
+
2271
+ const wrappedHandler: WSHandler<unknown> = {
2272
+ onClose: (_ctx, _ws, _code, reason) => {
2273
+ if (!conn) return;
2274
+ const handlers = conn._handlers;
2275
+ if (handlers.close) {
2276
+ handlers.close(reason || "normal");
2277
+ }
2278
+ bus.unsubscribeAll(connId);
2279
+ bus.removeHandler(connId);
2280
+ },
2281
+ onMessage: (_ctx, _ws, data) => {
2282
+ if (!conn) return;
2283
+ const handlers = conn._handlers;
2284
+ if (handlers.message && typeof data === "string") {
2285
+ try {
2286
+ const parsed: unknown = JSON.parse(data);
2287
+ if (isMsgFrame(parsed)) {
2288
+ handlers.message(parsed.data);
2289
+ }
2290
+ } catch { /* ignore malformed frames */ }
2291
+ }
2292
+ },
2293
+ onOpen: (_ctx, ws) => {
2294
+ if (!socket) initConn(ws);
2295
+ },
2296
+ };
2297
+
2298
+ const upgradeResult = await wsAdapter.upgrade(fc.request, fc.env, wrappedHandler);
2299
+
2300
+ /* Node/CF adapters return the socket from upgrade(); Bun returns undefined (socket comes via onOpen).
2301
+ * Deno pre-upgrade may still be CONNECTING — wait for onOpen so the first send is not dropped. */
2302
+ if (upgradeResult.socket && !socket && upgradeResult.socket.readyState === 1) {
2303
+ initConn(upgradeResult.socket);
2304
+ }
2305
+
2306
+ return upgradeResult.response;
2307
+ });
2308
+ } catch (thrown) {
2309
+ return this._toErrorResponse(thrown);
2310
+ }
2311
+ }
2312
+
2313
+ private async _handle404(
2314
+ fc: FetchCtx<TEnv>,
2315
+ method: string,
2316
+ path: string,
2317
+ ): Promise<Response> {
2318
+ safeFire(
2319
+ () => this._telemetry?.onNotFound?.({ method, path, req: fc.request }),
2320
+ fc.log,
2321
+ );
2322
+ const make404 = () => {
2323
+ if (this._onNotFound) {
2324
+ return this._onNotFound(this._makeErrorCtx(fc));
2325
+ }
2326
+ return this._makeErrorCtx(fc).jsonFromError(
2327
+ this._createError(EK.not_found, SK.not_found),
2328
+ );
2329
+ };
2330
+ try {
2331
+ const ctx404 = new HoneyContext({
2332
+ env: fc.env,
2333
+ executionCtx: fc.executionCtx,
2334
+ params: {},
2335
+ req: fc.request,
2336
+ urlFn: fc.url,
2337
+ });
2338
+ if (this._contextValues) Object.assign(ctx404, this._contextValues);
2339
+ const res =
2340
+ this._chainMiddlewares.length > 0
2341
+ ? await executeChain(this._chainMiddlewares, ctx404, make404)
2342
+ : await make404();
2343
+ safeFire(
2344
+ () =>
2345
+ this._telemetry?.onResponse?.({
2346
+ duration: performance.now() - fc.startTime,
2347
+ req: fc.request,
2348
+ status: res.status,
2349
+ }),
2350
+ fc.log,
2351
+ );
2352
+ return res;
2353
+ } catch (thrown) {
2354
+ return this._toErrorResponse(thrown);
2355
+ }
2356
+ }
2357
+
2358
+ private _preflightFallbackMethod(allowed: string[]): HttpMethod {
2359
+ if (allowed.includes("GET")) return "GET";
2360
+ if (allowed.includes("HEAD")) return "HEAD";
2361
+ if (allowed.includes("POST")) return "POST";
2362
+ return allowed[0] as HttpMethod;
2363
+ }
2364
+
2365
+ /** Run the existing method's middleware for a CORS preflight. Do not invoke the route handler. */
2366
+ private async _handleCorsPreflight(
2367
+ fc: FetchCtx<TEnv>,
2368
+ path: string,
2369
+ handler: RouteHandler,
2370
+ params: Record<string, string>,
2371
+ allowed: string[],
2372
+ ): Promise<Response> {
2373
+ const ctx = new HoneyContext({
2374
+ env: fc.env,
2375
+ executionCtx: fc.executionCtx,
2376
+ meta: handler.mt ? Object.freeze(handler.mt) : undefined,
2377
+ params,
2378
+ path,
2379
+ req: fc.request,
2380
+ routePattern: handler.rp,
2381
+ urlFn: fc.url,
2382
+ });
2383
+ if (this._contextValues) Object.assign(ctx, this._contextValues);
2384
+ try {
2385
+ return await executeChain(
2386
+ [...this._globalMiddlewares, ...handler.mw],
2387
+ ctx,
2388
+ () => this._handle405(fc, "OPTIONS", path, allowed),
2389
+ );
2390
+ } catch (thrown) {
2391
+ return this._toErrorResponse(thrown);
2392
+ }
2393
+ }
2394
+
2395
+ private async _handle405(
2396
+ fc: FetchCtx<TEnv>,
2397
+ method: string,
2398
+ path: string,
2399
+ allowed: string[],
2400
+ ): Promise<Response> {
2401
+ safeFire(
2402
+ () =>
2403
+ this._telemetry?.onMethodNotAllowed?.({
2404
+ allowed,
2405
+ method,
2406
+ path,
2407
+ req: fc.request,
2408
+ }),
2409
+ fc.log,
2410
+ );
2411
+ const make405 = async () => {
2412
+ if (this._onMethodNotAllowed) {
2413
+ const res = await this._onMethodNotAllowed(
2414
+ this._makeErrorCtx(fc, allowed) as {
2415
+ allowed: string[];
2416
+ env: TEnv;
2417
+ jsonFromError: (err: HoneyError) => Response;
2418
+ req: Request;
2419
+ },
2420
+ );
2421
+ const responseHeaders = new Headers(res.headers);
2422
+ responseHeaders.set("allow", allowed.join(", "));
2423
+ return new Response(res.body, {
2424
+ headers: responseHeaders,
2425
+ status: res.status,
2426
+ });
2427
+ }
2428
+ const err = this._createError(
2429
+ EK.method_not_allowed,
2430
+ SK.method_not_allowed,
2431
+ );
2432
+ const res = this._makeErrorCtx(fc).jsonFromError(err);
2433
+ const responseHeaders = new Headers(res.headers);
2434
+ responseHeaders.set("allow", allowed.join(", "));
2435
+ return new Response(res.body, {
2436
+ headers: responseHeaders,
2437
+ status: res.status,
2438
+ });
2439
+ };
2440
+ try {
2441
+ const ctx405 = new HoneyContext({
2442
+ env: fc.env,
2443
+ executionCtx: fc.executionCtx,
2444
+ params: {},
2445
+ req: fc.request,
2446
+ urlFn: fc.url,
2447
+ });
2448
+ if (this._contextValues) Object.assign(ctx405, this._contextValues);
2449
+ const finalRes =
2450
+ this._chainMiddlewares.length > 0
2451
+ ? await executeChain(this._chainMiddlewares, ctx405, make405)
2452
+ : await make405();
2453
+ safeFire(
2454
+ () =>
2455
+ this._telemetry?.onResponse?.({
2456
+ duration: performance.now() - fc.startTime,
2457
+ req: fc.request,
2458
+ status: finalRes.status,
2459
+ }),
2460
+ fc.log,
2461
+ );
2462
+ return finalRes;
2463
+ } catch (thrown) {
2464
+ return this._toErrorResponse(thrown);
2465
+ }
2466
+ }
2467
+
2468
+ private _handleMatched(
2469
+ fc: FetchCtx<TEnv>,
2470
+ method: HttpMethod,
2471
+ path: string,
2472
+ handler: RouteHandler,
2473
+ params: Record<string, string>,
2474
+ stashedMeta?: Record<string, unknown> | null,
2475
+ ): Response | Promise<Response> {
2476
+ const { env, executionCtx, log, request } = fc;
2477
+
2478
+ /* resolve error factory — pre-computed ef preferred, else build/use global */
2479
+ let errors: Record<string, (...args: never[]) => unknown> | undefined;
2480
+ if (handler.ef !== null) {
2481
+ errors = handler.ef;
2482
+ } else if (this._errorFactory !== null) {
2483
+ if (handler.ek.size > 0) {
2484
+ const factory = this._errorFactory as Record<
2485
+ string,
2486
+ (...args: never[]) => unknown
2487
+ >;
2488
+ const subset = Object.create(null) as Record<string, unknown>;
2489
+ for (const key of handler.ek) {
2490
+ if (key in factory) {
2491
+ subset[key] = factory[key];
2492
+ }
2493
+ }
2494
+ errors = Object.freeze(subset) as Record<
2495
+ string,
2496
+ (...args: never[]) => unknown
2497
+ >;
2498
+ } else {
2499
+ errors = this._errorFactory as Record<
2500
+ string,
2501
+ (...args: never[]) => unknown
2502
+ >;
2503
+ }
2504
+ }
2505
+
2506
+ /* meta: stashed meta from fn:null tree match takes priority over handler meta */
2507
+ const resolvedMeta = stashedMeta ?? handler.mt;
2508
+
2509
+ const ctx = new HoneyContext({
2510
+ env,
2511
+ executionCtx,
2512
+ meta: resolvedMeta ?? undefined,
2513
+ params,
2514
+ path,
2515
+ req: request,
2516
+ routePattern: handler.rp,
2517
+ urlFn: fc.url,
2518
+ });
2519
+ if (this._contextValues) Object.assign(ctx, this._contextValues);
2520
+ if (this._realtimeBus) {
2521
+ const rtBus = this._realtimeBus;
2522
+ Object.assign(ctx, {
2523
+ realtime: { publish: (topic: string, data: unknown) => rtBus.publish(topic, data) },
2524
+ });
2525
+ }
2526
+ if (errors) {
2527
+ ctx._setErrors(errors);
2528
+ }
2529
+
2530
+ if (this._telemetry !== null) {
2531
+ try {
2532
+ this._telemetry.onRoute?.({ method, params, path, req: request });
2533
+ } catch {
2534
+ /* telemetry must not crash request */
2535
+ }
2536
+ }
2537
+
2538
+ /*
2539
+ * Tier 3: Use pre-compiled chain when possible.
2540
+ * Compiled chains are cached on the handler — created once, reused per request.
2541
+ * Falls back to dynamic assembly when telemetry wrapping, input validation, or
2542
+ * scoped middleware is in play (scoped mw cannot be baked into the compiled cache
2543
+ * because each route may match a different subset).
2544
+ */
2545
+ const hasTelemetryMw = this._telemetry?.onMiddleware !== undefined;
2546
+ const hasInputValidation = handler.iv !== null;
2547
+
2548
+ /*
2549
+ * Error resolver — stored on ctx so the cached handler wrapper can read it.
2550
+ * Converts handler errors into error Responses inside the middleware chain,
2551
+ * allowing all post-next() middleware code (headers, logging, timing) to run.
2552
+ */
2553
+ ctx._errorToResponse = (thrown: unknown) =>
2554
+ this._resolveErrorResponse(thrown, handler, fc, method, path, ctx);
2555
+
2556
+ const onError = (thrown: unknown): Response | Promise<Response> => {
2557
+ if (ctx._errorToResponse) return ctx._errorToResponse(thrown);
2558
+ return this._toErrorResponse(thrown);
2559
+ };
2560
+
2561
+ const after = (response: Response): Response | Promise<Response> => {
2562
+ try {
2563
+ const done = this._afterMatched(fc, method, path, handler, ctx, response);
2564
+ if (done instanceof Promise) return done.catch(onError);
2565
+ return done;
2566
+ } catch (thrown) {
2567
+ return onError(thrown);
2568
+ }
2569
+ };
2570
+
2571
+ try {
2572
+ if (!hasTelemetryMw && !hasInputValidation && this._scopedMiddlewares.length === 0) {
2573
+ if (!handler._compiled) {
2574
+ const chainMw = this._chainMiddlewares;
2575
+ const handlerHasChain =
2576
+ chainMw.length > 0 && chainMw.every((mw, i) => handler.mw[i] === mw);
2577
+ const allMw = handlerHasChain
2578
+ ? [...this._globalMiddlewares, ...handler.mw]
2579
+ : [...this._globalMiddlewares, ...chainMw, ...handler.mw];
2580
+ handler._compiled = compileChain(allMw, (c) => {
2581
+ try {
2582
+ const result = handler.fn(c);
2583
+ if (result instanceof Promise) {
2584
+ return result.catch((thrown: unknown) => {
2585
+ const hCtx = c as HoneyContext<TEnv>;
2586
+ if (hCtx._errorToResponse)
2587
+ return hCtx._errorToResponse(thrown);
2588
+ throw thrown;
2589
+ });
2590
+ }
2591
+ return result;
2592
+ } catch (thrown) {
2593
+ const hCtx = c as HoneyContext<TEnv>;
2594
+ if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
2595
+ throw thrown;
2596
+ }
2597
+ });
2598
+ }
2599
+ const result = handler._compiled(ctx);
2600
+ if (result instanceof Promise) return result.then(after, onError);
2601
+ return after(result);
2602
+ }
2603
+
2604
+ const scopedForPath = this._filterScopedForPath(handler.rp);
2605
+ const chainMw = this._chainMiddlewares;
2606
+ const handlerHasChain =
2607
+ chainMw.length > 0 && chainMw.every((mw, i) => handler.mw[i] === mw);
2608
+ /*
2609
+ * Ordering: [global → chain → scoped → handler-route-specific]
2610
+ * When handlerHasChain, handler.mw = [chain..., routeSpecific...].
2611
+ * Scoped must go after chain but before route-specific, so we split.
2612
+ */
2613
+ let allMiddlewares: RuntimeMiddleware[] = handlerHasChain
2614
+ ? [
2615
+ ...this._globalMiddlewares,
2616
+ ...handler.mw.slice(0, chainMw.length),
2617
+ ...scopedForPath,
2618
+ ...handler.mw.slice(chainMw.length),
2619
+ ]
2620
+ : [...this._globalMiddlewares, ...chainMw, ...scopedForPath, ...handler.mw];
2621
+
2622
+ if (hasTelemetryMw) {
2623
+ const onMw = this._telemetry?.onMiddleware;
2624
+ if (onMw) {
2625
+ allMiddlewares = allMiddlewares.map((mw) => {
2626
+ const name = mw.name || "anonymous";
2627
+ const wrapped: RuntimeMiddleware = async (wCtx, wNext) => {
2628
+ const mwStart = performance.now();
2629
+ try {
2630
+ const res = await mw(wCtx, wNext);
2631
+ safeFire(
2632
+ () => onMw({ duration: performance.now() - mwStart, name }),
2633
+ log,
2634
+ );
2635
+ return res;
2636
+ } catch (error) {
2637
+ safeFire(
2638
+ () =>
2639
+ onMw({
2640
+ duration: performance.now() - mwStart,
2641
+ error,
2642
+ name,
2643
+ }),
2644
+ log,
2645
+ );
2646
+ throw error;
2647
+ }
2648
+ };
2649
+ return wrapped;
2650
+ });
2651
+ }
2652
+ }
2653
+
2654
+ if (hasInputValidation) {
2655
+ const schemas = handler.iv;
2656
+ if (schemas) {
2657
+ const inputMw: RuntimeMiddleware = async (inputCtx, inputNext) => {
2658
+ const validated = await validateInput(
2659
+ schemas,
2660
+ inputCtx["req"] as Request,
2661
+ params,
2662
+ );
2663
+ return inputNext({ input: validated });
2664
+ };
2665
+ allMiddlewares.push(inputMw);
2666
+ }
2667
+ }
2668
+
2669
+ return executeChain(allMiddlewares, ctx, (finalCtx) => {
2670
+ try {
2671
+ const result = handler.fn(finalCtx);
2672
+ if (result instanceof Promise) {
2673
+ return result.catch((thrown: unknown) => {
2674
+ const hCtx = finalCtx as HoneyContext<TEnv>;
2675
+ if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
2676
+ throw thrown;
2677
+ });
2678
+ }
2679
+ return result;
2680
+ } catch (thrown) {
2681
+ const hCtx = finalCtx as HoneyContext<TEnv>;
2682
+ if (hCtx._errorToResponse) return hCtx._errorToResponse(thrown);
2683
+ throw thrown;
2684
+ }
2685
+ }).then(after, onError);
2686
+ } catch (thrown) {
2687
+ /* safety net — middleware-level errors (input validation, middleware crash) */
2688
+ return onError(thrown);
2689
+ }
2690
+ }
2691
+
2692
+ private _afterMatched(
2693
+ fc: FetchCtx<TEnv>,
2694
+ method: HttpMethod,
2695
+ path: string,
2696
+ handler: RouteHandler,
2697
+ ctx: HoneyContext<TEnv>,
2698
+ response: Response,
2699
+ ): Response | Promise<Response> {
2700
+ const validateOut =
2701
+ this._outputValidation === "always" ||
2702
+ (this._outputValidation === "dev" &&
2703
+ (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env
2704
+ ?.NODE_ENV !== "production")
2705
+ if (
2706
+ handler.os &&
2707
+ validateOut &&
2708
+ response.body !== null &&
2709
+ !ctx._isErrorResponse
2710
+ ) {
2711
+ return this._validateThenFinish(fc, method, path, handler, ctx, response);
2712
+ }
2713
+ return this._finishMatched(fc, method, path, handler, ctx, response);
2714
+ }
2715
+
2716
+ private async _validateThenFinish(
2717
+ fc: FetchCtx<TEnv>,
2718
+ method: HttpMethod,
2719
+ path: string,
2720
+ handler: RouteHandler,
2721
+ ctx: HoneyContext<TEnv>,
2722
+ response: Response,
2723
+ ): Promise<Response> {
2724
+ const ct = response.headers.get("content-type");
2725
+
2726
+ /* content-type mismatch check */
2727
+ if (ct && handler.os) {
2728
+ const declaredTypes = Object.keys(handler.os);
2729
+ const matches = declaredTypes.some((t) => ct.startsWith(t));
2730
+ if (!matches) {
2731
+ throw new HoneyError({
2732
+ errorKey: EK.output_content_type_mismatch,
2733
+ status: SK.internal_server_error,
2734
+ });
2735
+ }
2736
+ }
2737
+
2738
+ /* JSON schema validation — read original, return clone (Bun clone() drains original) */
2739
+ if (ct?.startsWith("application/json") && handler.ov) {
2740
+ const sk = codeToStatusKey[response.status];
2741
+ if (sk) {
2742
+ const forReturn = response.clone();
2743
+ const data: unknown = await response.json();
2744
+ await handler.ov(sk, data);
2745
+ response = forReturn;
2746
+ }
2747
+ }
2748
+ return this._finishMatched(fc, method, path, handler, ctx, response);
2749
+ }
2750
+
2751
+ private _finishMatched(
2752
+ fc: FetchCtx<TEnv>,
2753
+ method: HttpMethod,
2754
+ path: string,
2755
+ handler: RouteHandler,
2756
+ ctx: HoneyContext<TEnv>,
2757
+ response: Response,
2758
+ ): Response {
2759
+ /* taps — fire after successful handler, non-blocking */
2760
+ if (this._taps !== null && !ctx._isErrorResponse) {
2761
+ const taps = this._taps;
2762
+ const log = fc.log;
2763
+
2764
+ /* meta-driven taps — fire for each registered key found in route meta */
2765
+ if (handler.mt !== null) {
2766
+ for (const [key, tapFn] of taps) {
2767
+ const metaValue = handler.mt[key];
2768
+ if (metaValue !== undefined) {
2769
+ ctx.background(
2770
+ Promise.resolve()
2771
+ .then(() => tapFn(ctx, metaValue))
2772
+ .catch((e) => log?.warn?.("tap failed", key, e)),
2773
+ );
2774
+ }
2775
+ }
2776
+ }
2777
+
2778
+ /* dynamic taps — fire for each c.tap() call */
2779
+ if (ctx._pendingTaps !== null) {
2780
+ for (const pending of ctx._pendingTaps) {
2781
+ const tapFn = taps.get(pending.key);
2782
+ if (tapFn !== undefined) {
2783
+ ctx.background(
2784
+ Promise.resolve()
2785
+ .then(() => tapFn(ctx, pending.payload))
2786
+ .catch((e) => log?.warn?.("tap failed", pending.key, e)),
2787
+ );
2788
+ }
2789
+ }
2790
+ ctx._pendingTaps = null;
2791
+ }
2792
+ }
2793
+
2794
+ if (this._telemetry !== null) {
2795
+ try {
2796
+ const duration = performance.now() - fc.startTime;
2797
+ this._telemetry.onHandler?.({
2798
+ duration,
2799
+ method,
2800
+ path,
2801
+ status: response.status,
2802
+ });
2803
+ this._telemetry.onResponse?.({
2804
+ duration,
2805
+ req: fc.request,
2806
+ status: response.status,
2807
+ });
2808
+ } catch {
2809
+ /* telemetry must never crash the response path */
2810
+ }
2811
+ }
2812
+ /* HEAD responses must have empty body — preserve headers + status */
2813
+ if (method === "HEAD") {
2814
+ return new Response(null, {
2815
+ headers: response.headers,
2816
+ status: response.status,
2817
+ });
2818
+ }
2819
+ return response;
2820
+ }
2821
+ }
2822
+
2823
+ /*
2824
+ * Handler Type Algebra
2825
+ * These types compose the handler context from route configuration.
2826
+ * NarrowMethod → ApplyOutput → ApplyParams → HandlerCtx
2827
+ */
2828
+
2829
+ /**
2830
+ * When content type IS declared → narrow the method (constrain status keys + body).
2831
+ * When content type NOT declared → remove the method (Omit).
2832
+ * Narrowed methods return TypedResponse<CT, K> for compile-time CT+SK safety.
2833
+ */
2834
+ /** Resolve body type: schema → InferOutput, plain type → use as-is */
2835
+ type ResolveBody<T, TBody> = TBody extends "infer"
2836
+ ? T extends StandardSchemaLike
2837
+ ? InferOutput<T>
2838
+ : T
2839
+ : TBody;
2840
+
2841
+ type NarrowMethod<
2842
+ TRes,
2843
+ TSchemas,
2844
+ Method extends string,
2845
+ CT extends string,
2846
+ TBody,
2847
+ > = [TSchemas] extends [never]
2848
+ ? Omit<TRes, Method>
2849
+ : TSchemas extends Record<string, unknown>
2850
+ ? Omit<TRes, Method> & {
2851
+ [M in Method]: <K extends keyof TSchemas & string>(
2852
+ statusKey: K,
2853
+ body: ResolveBody<TSchemas[K], TBody>,
2854
+ opts?: ResponseOptions,
2855
+ ) => TypedResponse<CT, K>;
2856
+ }
2857
+ : Omit<TRes, Method>;
2858
+
2859
+ /**
2860
+ * SSE uses a callback signature, not statusKey+body like other methods.
2861
+ * Gate presence on text/event-stream declaration, preserve original signature.
2862
+ */
2863
+ type NarrowSSE<TOutput> = [
2864
+ ExtractSchemas<TOutput, "text/event-stream">,
2865
+ ] extends [never]
2866
+ ? {}
2867
+ : { sse: HoneyRes["sse"] };
2868
+
2869
+ /** Universal methods — always available, not gated by output declaration */
2870
+ type UniversalRes = Pick<HoneyRes, "noContent" | "raw" | "redirect" | "stream">;
2871
+
2872
+ /**
2873
+ * Constrain ctx.res when output schemas are declared.
2874
+ * Declared content types → method constrained (status keys + body type).
2875
+ * Undeclared content types → method removed.
2876
+ * Universal methods (noContent, redirect, stream, raw) always available.
2877
+ * HoneyContext has no private brand, so Omit<TCtx, "res"> replaces the wide HoneyRes.
2878
+ */
2879
+ type ApplyOutput<TCtx, TOutput> = [keyof TOutput] extends [never]
2880
+ ? TCtx
2881
+ : Omit<TCtx, "res"> & {
2882
+ readonly res: UniversalRes &
2883
+ NarrowMethod<
2884
+ {},
2885
+ ExtractSchemas<TOutput, "application/json">,
2886
+ "json",
2887
+ "application/json",
2888
+ "infer"
2889
+ > &
2890
+ NarrowMethod<
2891
+ {},
2892
+ ExtractSchemas<TOutput, "text/plain">,
2893
+ "text",
2894
+ "text/plain",
2895
+ string
2896
+ > &
2897
+ NarrowMethod<
2898
+ {},
2899
+ ExtractSchemas<TOutput, "text/html">,
2900
+ "html",
2901
+ "text/html",
2902
+ string
2903
+ > &
2904
+ NarrowMethod<
2905
+ {},
2906
+ ExtractSchemas<TOutput, "application/xml">,
2907
+ "xml",
2908
+ "application/xml",
2909
+ string
2910
+ > &
2911
+ NarrowMethod<
2912
+ {},
2913
+ ExtractSchemas<TOutput, "text/csv">,
2914
+ "csv",
2915
+ "text/csv",
2916
+ string
2917
+ > &
2918
+ NarrowMethod<
2919
+ {},
2920
+ ExtractSchemas<TOutput, "application/octet-stream">,
2921
+ "binary",
2922
+ "application/octet-stream",
2923
+ ArrayBuffer | Uint8Array<ArrayBuffer>
2924
+ > &
2925
+ NarrowSSE<TOutput>;
2926
+ };
2927
+
2928
+ /** Public alias for codegen — applies output schema constraints to a context type */
2929
+ export type WithOutput<TCtx, TOutput> = ApplyOutput<TCtx, TOutput>;
2930
+
2931
+ /** @internal — tuple entry describing a scoped middleware at the type level */
2932
+ type ScopedMwEntry = { readonly path: string; readonly adds: unknown }
2933
+
2934
+ /**
2935
+ * Walk TScopedMw tuple, intersect `adds` for every entry whose `path` is a prefix of TFullPath.
2936
+ * Prefix semantics: TFullPath extends `${P}` (exact) | `${P}/${string}` (descendant).
2937
+ *
2938
+ * Non-literal-path guard: if Head["path"] is the base `string` type (happens when
2939
+ * the user passes a widened variable), skip the entry via `string extends Head["path"] ? {}`.
2940
+ * This prevents the always-true `'/anywhere' extends string` from polluting every route.
2941
+ */
2942
+ type ApplyScoped<
2943
+ TScopedMw extends readonly ScopedMwEntry[],
2944
+ TFullPath extends string,
2945
+ > = TScopedMw extends readonly [
2946
+ infer Head extends ScopedMwEntry,
2947
+ ...infer Rest extends readonly ScopedMwEntry[],
2948
+ ]
2949
+ ? (string extends Head["path"]
2950
+ ? {}
2951
+ : TFullPath extends Head["path"]
2952
+ ? Head["adds"]
2953
+ : TFullPath extends `${Head["path"] & string}/${string}`
2954
+ ? Head["adds"]
2955
+ : {}) &
2956
+ ApplyScoped<Rest, TFullPath>
2957
+ : {}
2958
+
2959
+ /** @internal — runtime entry for a scoped middleware */
2960
+ type ScopedEntry = {
2961
+ /** merged full-path prefix (already rebased against basePath at .use time) */
2962
+ prefix: string
2963
+ mw: RuntimeMiddleware
2964
+ /** cached from mw.errors at registration; undefined when none */
2965
+ errors: readonly string[] | undefined
2966
+ }
2967
+
2968
+ /** Apply typed params — override params with specific keys when route has :param segments */
2969
+ type ApplyParams<TCtx, TParams> = [keyof TParams] extends [string]
2970
+ ? string extends keyof TParams
2971
+ ? TCtx
2972
+ : TCtx & { readonly params: TParams }
2973
+ : TCtx;
2974
+
2975
+ /** Typed tap method — constrained to registered tap keys when TTaps is non-empty */
2976
+ type TypedTap<TTaps extends Record<string, unknown>> =
2977
+ [keyof TTaps] extends [never]
2978
+ ? { tap(key: string, payload: unknown): void }
2979
+ : { tap<K extends string & keyof TTaps>(key: K, payload: TTaps[K]): void };
2980
+
2981
+ /** Build the full handler context: base ctx + params + input + meta + errors + taps + output-constrained methods */
2982
+ type HandlerCtx<
2983
+ TCtx,
2984
+ TInput,
2985
+ TOutput,
2986
+ TParams,
2987
+ TAccMeta = {},
2988
+ TErrorFactory = never,
2989
+ TErrorKeys extends string = never,
2990
+ TPath extends string = string,
2991
+ TTaps extends Record<string, unknown> = {},
2992
+ > = ApplyOutput<
2993
+ ApplyParams<
2994
+ [keyof TInput] extends [never] ? TCtx : TCtx & { input: TInput },
2995
+ TParams
2996
+ > & {
2997
+ readonly meta: Readonly<Omit<TAccMeta, "openApi">>;
2998
+ readonly routePattern: TPath;
2999
+ } & ([TErrorFactory] extends [never]
3000
+ ? {}
3001
+ : [TErrorKeys] extends [never]
3002
+ ? { readonly errors: TErrorFactory }
3003
+ : {
3004
+ readonly errors: Pick<
3005
+ TErrorFactory,
3006
+ TErrorKeys & keyof TErrorFactory
3007
+ >;
3008
+ })
3009
+ & TypedTap<TTaps>,
3010
+ TOutput
3011
+ >;
3012
+
3013
+ /** @internal — exposes private Honey members for RouteBuilder/WSRouteBuilder access */
3014
+ type HoneyInternal = {
3015
+ _factory: unknown;
3016
+ _markWsRoutes(): void;
3017
+ _registerStatic(key: string, handler: RouteHandler): void;
3018
+ };
3019
+
3020
+ type RouteBuilderState<TParent> = {
3021
+ boundaryErrorKey: string | null;
3022
+ errorKeys: Set<string>;
3023
+ extraMethods: (HttpMethod | "ALL")[] | null;
3024
+ handlerMap: Record<string, RouteHandler> | null;
3025
+ inputSchemas: InputSchemasDef | null;
3026
+ meta: Record<string, unknown> | null;
3027
+ method: HttpMethod | "ALL";
3028
+ middlewares: RuntimeMiddleware[];
3029
+ outputSchemas: OutputSchemaDef | null;
3030
+ parent: TParent;
3031
+ parentMiddlewares: RuntimeMiddleware[];
3032
+ path: string;
3033
+ root: TreeNode;
3034
+ };
3035
+
3036
+ /** Honey.TMeta defaults to never; never & X is never, so start acc meta at {}. */
3037
+ type InitAccMeta<TMeta> = [TMeta] extends [never] ? {} : TMeta
3038
+
3039
+ /** Skip $routes merge only for a real `{ internal: true }` meta — not for `never`. */
3040
+ type SkipRouteRecord<TAccMeta> = [TAccMeta] extends [never]
3041
+ ? false
3042
+ : [TAccMeta] extends [{ internal: true }]
3043
+ ? true
3044
+ : false
3045
+
3046
+ /** Return type for handler() — extracted to avoid 3x duplication */
3047
+ type HandlerReturn<
3048
+ TEnv,
3049
+ TBaseCtx,
3050
+ TRoutes,
3051
+ TPath extends string,
3052
+ TMethod extends string,
3053
+ TInput,
3054
+ TOutput,
3055
+ TCtx,
3056
+ TAccMeta,
3057
+ TErrorFactory,
3058
+ _TErrorKeys extends string,
3059
+ TDefaultErrors extends string,
3060
+ TMeta,
3061
+ TBasePath extends string = "/",
3062
+ TTaps extends Record<string, unknown> = {},
3063
+ TScopedMw extends readonly ScopedMwEntry[] = [],
3064
+ > = Honey<
3065
+ TEnv,
3066
+ TBaseCtx,
3067
+ SkipRouteRecord<TAccMeta> extends true
3068
+ ? TRoutes
3069
+ : MergeRoute<
3070
+ TRoutes,
3071
+ TPath,
3072
+ TMethod,
3073
+ TInput,
3074
+ TOutput,
3075
+ HandlerCtx<
3076
+ TCtx,
3077
+ TInput,
3078
+ TOutput,
3079
+ ParamsFromPath<TPath>,
3080
+ TAccMeta,
3081
+ TErrorFactory,
3082
+ _TErrorKeys | TDefaultErrors,
3083
+ TPath,
3084
+ TTaps
3085
+ >,
3086
+ TAccMeta,
3087
+ _TErrorKeys | TDefaultErrors,
3088
+ ComputeErrorsByStatus<TErrorFactory, _TErrorKeys | TDefaultErrors, typeof ERROR_META>
3089
+ >,
3090
+ TMeta,
3091
+ TErrorFactory,
3092
+ TDefaultErrors,
3093
+ TBasePath,
3094
+ TTaps,
3095
+ TScopedMw
3096
+ >;
3097
+
3098
+ type OneShotKey = "boundary" | "errors" | "input" | "meta" | "output";
3099
+
3100
+ type BuilderChain<
3101
+ TEnv,
3102
+ TCtx,
3103
+ TInput,
3104
+ TErrorKeys extends string,
3105
+ TOutput,
3106
+ TPath extends string,
3107
+ TMethod extends string,
3108
+ TRoutes,
3109
+ TUsed extends string,
3110
+ TBaseCtx,
3111
+ TMeta,
3112
+ TAccMeta,
3113
+ TErrorFactory,
3114
+ TDefaultErrors extends string,
3115
+ TBasePath extends string,
3116
+ TTaps extends Record<string, unknown> = {},
3117
+ TScopedMw extends readonly ScopedMwEntry[] = [],
3118
+ > = Omit<
3119
+ RouteBuilder<
3120
+ TEnv, TCtx, TInput, TErrorKeys, TOutput, TPath, TMethod,
3121
+ TRoutes, TUsed, TBaseCtx, TMeta, TAccMeta, TErrorFactory,
3122
+ TDefaultErrors, TBasePath, TTaps, TScopedMw
3123
+ >,
3124
+ TUsed & OneShotKey
3125
+ >;
3126
+
3127
+ class RouteBuilder<
3128
+ TEnv,
3129
+ TCtx,
3130
+ TInput = {},
3131
+ _TErrorKeys extends string = never,
3132
+ TOutput = {},
3133
+ TPath extends string = string,
3134
+ TMethod extends string = string,
3135
+ TRoutes = {},
3136
+ TUsed extends string = never,
3137
+ TBaseCtx = TCtx,
3138
+ TMeta = never,
3139
+ TAccMeta = {},
3140
+ TErrorFactory = never,
3141
+ TDefaultErrors extends string = never,
3142
+ TBasePath extends string = "/",
3143
+ TTaps extends Record<string, unknown> = {},
3144
+ TScopedMw extends readonly ScopedMwEntry[] = [],
3145
+ > {
3146
+ private _s: RouteBuilderState<
3147
+ Honey<
3148
+ TEnv,
3149
+ TBaseCtx,
3150
+ TRoutes,
3151
+ TMeta,
3152
+ TErrorFactory,
3153
+ TDefaultErrors,
3154
+ TBasePath,
3155
+ TTaps,
3156
+ TScopedMw
3157
+ >
3158
+ >;
3159
+
3160
+ constructor(
3161
+ state: RouteBuilderState<
3162
+ Honey<
3163
+ TEnv,
3164
+ TBaseCtx,
3165
+ TRoutes,
3166
+ TMeta,
3167
+ TErrorFactory,
3168
+ TDefaultErrors,
3169
+ TBasePath,
3170
+ TTaps,
3171
+ TScopedMw
3172
+ >
3173
+ >,
3174
+ ) {
3175
+ this._s = state;
3176
+ }
3177
+
3178
+ errors<
3179
+ TFactory extends Record<string, (...args: never[]) => unknown>,
3180
+ TKeys extends Exclude<keyof TFactory & string, TDefaultErrors>,
3181
+ >(
3182
+ factory: TFactory,
3183
+ ...keys: TKeys[]
3184
+ ): BuilderChain<
3185
+ TEnv,
3186
+ TCtx,
3187
+ TInput,
3188
+ _TErrorKeys | TKeys,
3189
+ TOutput,
3190
+ TPath,
3191
+ TMethod,
3192
+ TRoutes,
3193
+ TUsed | "errors",
3194
+ TBaseCtx,
3195
+ TMeta,
3196
+ TAccMeta,
3197
+ TErrorFactory,
3198
+ TDefaultErrors,
3199
+ TBasePath,
3200
+ TTaps,
3201
+ TScopedMw
3202
+ >;
3203
+ errors<
3204
+ TKeys extends [TErrorFactory] extends [never]
3205
+ ? never
3206
+ : Exclude<keyof TErrorFactory & string, TDefaultErrors>,
3207
+ >(
3208
+ ...keys: TKeys[]
3209
+ ): BuilderChain<
3210
+ TEnv,
3211
+ TCtx,
3212
+ TInput,
3213
+ _TErrorKeys | TKeys,
3214
+ TOutput,
3215
+ TPath,
3216
+ TMethod,
3217
+ TRoutes,
3218
+ TUsed | "errors",
3219
+ TBaseCtx,
3220
+ TMeta,
3221
+ TAccMeta,
3222
+ TErrorFactory,
3223
+ TDefaultErrors,
3224
+ TBasePath,
3225
+ TTaps,
3226
+ TScopedMw
3227
+ >;
3228
+ errors(
3229
+ ...args: unknown[]
3230
+ ): BuilderChain<
3231
+ TEnv,
3232
+ TCtx,
3233
+ TInput,
3234
+ string,
3235
+ TOutput,
3236
+ TPath,
3237
+ TMethod,
3238
+ TRoutes,
3239
+ TUsed | "errors",
3240
+ TBaseCtx,
3241
+ TMeta,
3242
+ TAccMeta,
3243
+ TErrorFactory,
3244
+ TDefaultErrors,
3245
+ TBasePath,
3246
+ TTaps,
3247
+ TScopedMw
3248
+ > {
3249
+ const keys =
3250
+ typeof args[0] === "object" && args[0] !== null
3251
+ ? (args.slice(1) as string[])
3252
+ : (args as string[]);
3253
+ for (const k of keys) {
3254
+ this._s.errorKeys.add(k);
3255
+ }
3256
+ return new RouteBuilder<
3257
+ TEnv,
3258
+ TCtx,
3259
+ TInput,
3260
+ string,
3261
+ TOutput,
3262
+ TPath,
3263
+ TMethod,
3264
+ TRoutes,
3265
+ TUsed | "errors",
3266
+ TBaseCtx,
3267
+ TMeta,
3268
+ TAccMeta,
3269
+ TErrorFactory,
3270
+ TDefaultErrors,
3271
+ TBasePath,
3272
+ TTaps,
3273
+ TScopedMw
3274
+ >({
3275
+ ...this._s,
3276
+ errorKeys: this._s.errorKeys,
3277
+ });
3278
+ }
3279
+
3280
+ boundary<
3281
+ TKey extends [TErrorFactory] extends [never]
3282
+ ? never
3283
+ : Exclude<keyof TErrorFactory & string, TDefaultErrors>,
3284
+ >(
3285
+ key: TKey,
3286
+ ): BuilderChain<
3287
+ TEnv,
3288
+ TCtx,
3289
+ TInput,
3290
+ _TErrorKeys | TKey,
3291
+ TOutput,
3292
+ TPath,
3293
+ TMethod,
3294
+ TRoutes,
3295
+ TUsed | "boundary",
3296
+ TBaseCtx,
3297
+ TMeta,
3298
+ TAccMeta,
3299
+ TErrorFactory,
3300
+ TDefaultErrors,
3301
+ TBasePath,
3302
+ TTaps,
3303
+ TScopedMw
3304
+ > {
3305
+ this._s.boundaryErrorKey = key;
3306
+ this._s.errorKeys.add(key);
3307
+ return new RouteBuilder<
3308
+ TEnv,
3309
+ TCtx,
3310
+ TInput,
3311
+ _TErrorKeys | TKey,
3312
+ TOutput,
3313
+ TPath,
3314
+ TMethod,
3315
+ TRoutes,
3316
+ TUsed | "boundary",
3317
+ TBaseCtx,
3318
+ TMeta,
3319
+ TAccMeta,
3320
+ TErrorFactory,
3321
+ TDefaultErrors,
3322
+ TBasePath,
3323
+ TTaps,
3324
+ TScopedMw
3325
+ >({
3326
+ ...this._s,
3327
+ boundaryErrorKey: key,
3328
+ errorKeys: this._s.errorKeys,
3329
+ });
3330
+ }
3331
+
3332
+ handler(
3333
+ fn: (
3334
+ ctx: HandlerCtx<
3335
+ TCtx,
3336
+ TInput,
3337
+ TOutput,
3338
+ ParamsFromPath<TPath>,
3339
+ TAccMeta,
3340
+ TErrorFactory,
3341
+ _TErrorKeys | TDefaultErrors,
3342
+ TPath,
3343
+ TTaps
3344
+ >,
3345
+ ) => TypedResponse | Promise<TypedResponse>,
3346
+ ): HandlerReturn<
3347
+ TEnv,
3348
+ TBaseCtx,
3349
+ TRoutes,
3350
+ TPath,
3351
+ TMethod,
3352
+ TInput,
3353
+ TOutput,
3354
+ TCtx,
3355
+ TAccMeta,
3356
+ TErrorFactory,
3357
+ _TErrorKeys,
3358
+ TDefaultErrors,
3359
+ TMeta,
3360
+ TBasePath,
3361
+ TTaps,
3362
+ TScopedMw
3363
+ > {
3364
+ let ov: OutputValidator | null = null;
3365
+ const outputSchemas = this._s.outputSchemas;
3366
+ if (outputSchemas) {
3367
+ const jsonSchemas = outputSchemas["application/json"];
3368
+ if (jsonSchemas) {
3369
+ ov = async (statusKey: string, data: unknown) => {
3370
+ const schema = jsonSchemas[statusKey as keyof typeof jsonSchemas];
3371
+ if (schema) {
3372
+ await validateOutput(schema, statusKey, data);
3373
+ }
3374
+ };
3375
+ }
3376
+ }
3377
+
3378
+ const isInternal = Symbol.for("honey.internal") in fn
3379
+ if (isInternal) {
3380
+ Object.defineProperty(fn, Symbol.for("honey.app"), { value: this._s.parent })
3381
+ }
3382
+
3383
+ const routeHandler: RouteHandler = {
3384
+ _skip: isInternal || undefined,
3385
+ bek: this._s.boundaryErrorKey,
3386
+ ef: null,
3387
+ ek: this._s.errorKeys,
3388
+ fn: fn as (ctx: unknown) => Response | Promise<Response>,
3389
+ iv: this._s.inputSchemas,
3390
+ mt: this._s.meta ? Object.freeze(this._s.meta) : null,
3391
+ mw: [...this._s.parentMiddlewares, ...this._s.middlewares],
3392
+ os: this._s.outputSchemas,
3393
+ ov,
3394
+ rp: this._s.path,
3395
+ };
3396
+
3397
+ /* patch mode: if handler map has this route, patch existing handler instead of insertRoute */
3398
+ const handlerMap = this._s.handlerMap;
3399
+ if (handlerMap) {
3400
+ const key = `${this._s.method} ${this._s.path}`;
3401
+ const existing = handlerMap[key];
3402
+ if (existing) {
3403
+ existing.fn = routeHandler.fn;
3404
+ existing.mw = routeHandler.mw;
3405
+ existing.iv = routeHandler.iv;
3406
+ existing.os = routeHandler.os;
3407
+ existing.ov = routeHandler.ov;
3408
+ /* pre-compute filtered error factory */
3409
+ const factory = (this._s.parent as unknown as HoneyInternal)._factory;
3410
+ if (factory !== null && existing.ek.size > 0) {
3411
+ const ef = Object.create(null) as Record<
3412
+ string,
3413
+ (...args: never[]) => unknown
3414
+ >;
3415
+ const fac = factory as Record<string, (...args: never[]) => unknown>;
3416
+ for (const k of existing.ek) {
3417
+ const factoryFn = fac[k]
3418
+ if (factoryFn !== undefined) {
3419
+ ef[k] = factoryFn
3420
+ }
3421
+ }
3422
+ existing.ef = Object.freeze(ef);
3423
+ }
3424
+ return this._s.parent as HandlerReturn<
3425
+ TEnv,
3426
+ TBaseCtx,
3427
+ TRoutes,
3428
+ TPath,
3429
+ TMethod,
3430
+ TInput,
3431
+ TOutput,
3432
+ TCtx,
3433
+ TAccMeta,
3434
+ TErrorFactory,
3435
+ _TErrorKeys,
3436
+ TDefaultErrors,
3437
+ TMeta,
3438
+ TBasePath,
3439
+ TTaps,
3440
+ TScopedMw
3441
+ >;
3442
+ }
3443
+ }
3444
+
3445
+ insertRoute(this._s.root, this._s.method, this._s.path, routeHandler);
3446
+
3447
+ /* .on() extra methods — insert same handler for each additional method */
3448
+ if (this._s.extraMethods) {
3449
+ for (const m of this._s.extraMethods) {
3450
+ insertRoute(this._s.root, m, this._s.path, routeHandler);
3451
+ if (!this._s.path.includes(":") && !this._s.path.includes("*")) {
3452
+ (this._s.parent as unknown as HoneyInternal)._registerStatic(`${m} ${this._s.path}`, routeHandler);
3453
+ }
3454
+ }
3455
+ }
3456
+
3457
+ /* populate static route map for O(1) lookup on non-parameterized routes */
3458
+ if (!this._s.path.includes(":") && !this._s.path.includes("*")) {
3459
+ (this._s.parent as unknown as HoneyInternal)._registerStatic(
3460
+ `${this._s.method} ${this._s.path}`,
3461
+ routeHandler,
3462
+ );
3463
+ }
3464
+ return this._s.parent as HandlerReturn<
3465
+ TEnv,
3466
+ TBaseCtx,
3467
+ TRoutes,
3468
+ TPath,
3469
+ TMethod,
3470
+ TInput,
3471
+ TOutput,
3472
+ TCtx,
3473
+ TAccMeta,
3474
+ TErrorFactory,
3475
+ _TErrorKeys,
3476
+ TDefaultErrors,
3477
+ TMeta,
3478
+ TBasePath,
3479
+ TTaps,
3480
+ TScopedMw
3481
+ >;
3482
+ }
3483
+
3484
+ proxy(
3485
+ config: ProxyConfig<
3486
+ HandlerCtx<
3487
+ TCtx,
3488
+ TInput,
3489
+ TOutput,
3490
+ ParamsFromPath<TPath>,
3491
+ TAccMeta,
3492
+ TErrorFactory,
3493
+ _TErrorKeys | TDefaultErrors,
3494
+ TPath,
3495
+ TTaps
3496
+ >
3497
+ >,
3498
+ ): HandlerReturn<
3499
+ TEnv,
3500
+ TBaseCtx,
3501
+ TRoutes,
3502
+ TPath,
3503
+ TMethod,
3504
+ TInput,
3505
+ TOutput,
3506
+ TCtx,
3507
+ TAccMeta,
3508
+ TErrorFactory,
3509
+ _TErrorKeys,
3510
+ TDefaultErrors,
3511
+ TMeta,
3512
+ TBasePath,
3513
+ TTaps,
3514
+ TScopedMw
3515
+ > {
3516
+ const proxyHandler = createProxyHandler(config);
3517
+ return this.handler(
3518
+ proxyHandler as (
3519
+ ctx: HandlerCtx<
3520
+ TCtx,
3521
+ TInput,
3522
+ TOutput,
3523
+ ParamsFromPath<TPath>,
3524
+ TAccMeta,
3525
+ TErrorFactory,
3526
+ _TErrorKeys | TDefaultErrors,
3527
+ TPath,
3528
+ TTaps
3529
+ >,
3530
+ ) => Promise<TypedResponse>,
3531
+ );
3532
+ }
3533
+
3534
+ input<TSchemas extends InputSchemasDef>(
3535
+ schemas: TSchemas,
3536
+ ): BuilderChain<
3537
+ TEnv,
3538
+ TCtx,
3539
+ TInput & InferInputMap<TSchemas>,
3540
+ _TErrorKeys,
3541
+ TOutput,
3542
+ TPath,
3543
+ TMethod,
3544
+ TRoutes,
3545
+ TUsed | "input",
3546
+ TBaseCtx,
3547
+ TMeta,
3548
+ TAccMeta,
3549
+ TErrorFactory,
3550
+ TDefaultErrors,
3551
+ TBasePath,
3552
+ TTaps,
3553
+ TScopedMw
3554
+ > {
3555
+ return new RouteBuilder<
3556
+ TEnv,
3557
+ TCtx,
3558
+ TInput & InferInputMap<TSchemas>,
3559
+ _TErrorKeys,
3560
+ TOutput,
3561
+ TPath,
3562
+ TMethod,
3563
+ TRoutes,
3564
+ TUsed | "input",
3565
+ TBaseCtx,
3566
+ TMeta,
3567
+ TAccMeta,
3568
+ TErrorFactory,
3569
+ TDefaultErrors,
3570
+ TBasePath,
3571
+ TTaps,
3572
+ TScopedMw
3573
+ >({
3574
+ ...this._s,
3575
+ inputSchemas: schemas,
3576
+ });
3577
+ }
3578
+
3579
+ meta<
3580
+ TRouteMeta extends Partial<DefaultMeta> &
3581
+ ([TMeta] extends [never] ? {} : TMeta),
3582
+ >(
3583
+ meta: TRouteMeta,
3584
+ ): BuilderChain<
3585
+ TEnv,
3586
+ TCtx,
3587
+ TInput,
3588
+ _TErrorKeys,
3589
+ TOutput,
3590
+ TPath,
3591
+ TMethod,
3592
+ TRoutes,
3593
+ TUsed | "meta",
3594
+ TBaseCtx,
3595
+ TMeta,
3596
+ TAccMeta & TRouteMeta,
3597
+ TErrorFactory,
3598
+ TDefaultErrors,
3599
+ TBasePath,
3600
+ TTaps,
3601
+ TScopedMw
3602
+ > {
3603
+ return new RouteBuilder<
3604
+ TEnv,
3605
+ TCtx,
3606
+ TInput,
3607
+ _TErrorKeys,
3608
+ TOutput,
3609
+ TPath,
3610
+ TMethod,
3611
+ TRoutes,
3612
+ TUsed | "meta",
3613
+ TBaseCtx,
3614
+ TMeta,
3615
+ TAccMeta & TRouteMeta,
3616
+ TErrorFactory,
3617
+ TDefaultErrors,
3618
+ TBasePath,
3619
+ TTaps,
3620
+ TScopedMw
3621
+ >({
3622
+ ...this._s,
3623
+ meta: (() => {
3624
+ const merged: Record<string, unknown> = { ...this._s.meta, ...meta }
3625
+ const inv = merged["invalidate"]
3626
+ if (Array.isArray(inv) && inv.length > 1) {
3627
+ merged["invalidate"] = [...new Set(inv)]
3628
+ }
3629
+ return merged
3630
+ })(),
3631
+ });
3632
+ }
3633
+
3634
+ output<TOutputSchemas extends OutputSchemaDef>(
3635
+ _schemas: TOutputSchemas,
3636
+ ): BuilderChain<
3637
+ TEnv,
3638
+ TCtx,
3639
+ TInput,
3640
+ _TErrorKeys,
3641
+ TOutputSchemas,
3642
+ TPath,
3643
+ TMethod,
3644
+ TRoutes,
3645
+ TUsed | "output",
3646
+ TBaseCtx,
3647
+ TMeta,
3648
+ TAccMeta,
3649
+ TErrorFactory,
3650
+ TDefaultErrors,
3651
+ TBasePath,
3652
+ TTaps,
3653
+ TScopedMw
3654
+ > {
3655
+ return new RouteBuilder<
3656
+ TEnv,
3657
+ TCtx,
3658
+ TInput,
3659
+ _TErrorKeys,
3660
+ TOutputSchemas,
3661
+ TPath,
3662
+ TMethod,
3663
+ TRoutes,
3664
+ TUsed | "output",
3665
+ TBaseCtx,
3666
+ TMeta,
3667
+ TAccMeta,
3668
+ TErrorFactory,
3669
+ TDefaultErrors,
3670
+ TBasePath,
3671
+ TTaps,
3672
+ TScopedMw
3673
+ >({
3674
+ ...this._s,
3675
+ outputSchemas: _schemas,
3676
+ });
3677
+ }
3678
+
3679
+ use<TAdds>(
3680
+ mw: MiddlewareFn<TCtx, TAdds>,
3681
+ ): BuilderChain<
3682
+ TEnv,
3683
+ TCtx & TAdds,
3684
+ TInput,
3685
+ _TErrorKeys,
3686
+ TOutput,
3687
+ TPath,
3688
+ TMethod,
3689
+ TRoutes,
3690
+ TUsed,
3691
+ TBaseCtx,
3692
+ TMeta,
3693
+ TAccMeta,
3694
+ TErrorFactory,
3695
+ TDefaultErrors,
3696
+ TBasePath,
3697
+ TTaps,
3698
+ TScopedMw
3699
+ > {
3700
+ if (mw.errors) {
3701
+ for (const k of mw.errors) {
3702
+ this._s.errorKeys.add(k);
3703
+ }
3704
+ }
3705
+ return new RouteBuilder<
3706
+ TEnv,
3707
+ TCtx & TAdds,
3708
+ TInput,
3709
+ _TErrorKeys,
3710
+ TOutput,
3711
+ TPath,
3712
+ TMethod,
3713
+ TRoutes,
3714
+ TUsed,
3715
+ TBaseCtx,
3716
+ TMeta,
3717
+ TAccMeta,
3718
+ TErrorFactory,
3719
+ TDefaultErrors,
3720
+ TBasePath,
3721
+ TTaps,
3722
+ TScopedMw
3723
+ >({
3724
+ ...this._s,
3725
+ middlewares: [...this._s.middlewares, mw as RuntimeMiddleware],
3726
+ });
3727
+ }
3728
+ }
3729
+
3730
+ type WSRouteBuilderState<TParent> = {
3731
+ boundaryErrorKey: string | null;
3732
+ errorKeys: Set<string>;
3733
+ inputSchemas: InputSchemasDef | null;
3734
+ meta: Record<string, unknown> | null;
3735
+ middlewares: RuntimeMiddleware[];
3736
+ parent: TParent;
3737
+ parentMiddlewares: RuntimeMiddleware[];
3738
+ path: string;
3739
+ root: TreeNode;
3740
+ };
3741
+
3742
+ class WSRouteBuilder<
3743
+ TEnv,
3744
+ TCtx,
3745
+ TInput = {},
3746
+ _TErrorKeys extends string = never,
3747
+ TParent extends { _markWsRoutes(): void } = { _markWsRoutes(): void },
3748
+ > {
3749
+ private _s: WSRouteBuilderState<TParent>;
3750
+
3751
+ constructor(state: WSRouteBuilderState<TParent>) {
3752
+ this._s = state;
3753
+ }
3754
+
3755
+ errors<TFactory extends Record<string, (...args: never[]) => unknown>>(
3756
+ factory: TFactory,
3757
+ ...keys: Array<keyof TFactory & string>
3758
+ ): WSRouteBuilder<
3759
+ TEnv,
3760
+ TCtx,
3761
+ TInput,
3762
+ _TErrorKeys | (keyof TFactory & string),
3763
+ TParent
3764
+ > {
3765
+ void factory;
3766
+ for (const k of keys) {
3767
+ this._s.errorKeys.add(k);
3768
+ }
3769
+ return new WSRouteBuilder<
3770
+ TEnv,
3771
+ TCtx,
3772
+ TInput,
3773
+ _TErrorKeys | (keyof TFactory & string),
3774
+ TParent
3775
+ >({
3776
+ ...this._s,
3777
+ errorKeys: this._s.errorKeys,
3778
+ });
3779
+ }
3780
+
3781
+ handler(wsHandler: WSHandler<TCtx>): TParent {
3782
+ const routeHandler: WSRouteHandler = {
3783
+ bek: this._s.boundaryErrorKey,
3784
+ ek: this._s.errorKeys,
3785
+ fn: wsHandler as WSHandler<unknown>,
3786
+ iv: this._s.inputSchemas,
3787
+ mt: this._s.meta ? Object.freeze(this._s.meta) : null,
3788
+ mw: [...this._s.parentMiddlewares, ...this._s.middlewares],
3789
+ rp: this._s.path,
3790
+ };
3791
+ insertWsRoute(this._s.root, this._s.path, routeHandler);
3792
+ ;(this._s.parent as unknown as HoneyInternal)._markWsRoutes();
3793
+ return this._s.parent;
3794
+ }
3795
+
3796
+ input<
3797
+ TSchemas extends Pick<InputSchemasDef, "cookies" | "headers" | "search">,
3798
+ >(
3799
+ schemas: TSchemas,
3800
+ ): WSRouteBuilder<
3801
+ TEnv,
3802
+ TCtx,
3803
+ TInput & InferInputMap<TSchemas>,
3804
+ _TErrorKeys,
3805
+ TParent
3806
+ > {
3807
+ return new WSRouteBuilder<
3808
+ TEnv,
3809
+ TCtx,
3810
+ TInput & InferInputMap<TSchemas>,
3811
+ _TErrorKeys,
3812
+ TParent
3813
+ >({
3814
+ ...this._s,
3815
+ inputSchemas: schemas,
3816
+ });
3817
+ }
3818
+
3819
+ meta(meta: Record<string, unknown>): this {
3820
+ this._s.meta = meta;
3821
+ return this;
3822
+ }
3823
+
3824
+ use<TAdds>(
3825
+ mw: MiddlewareFn<TCtx, TAdds>,
3826
+ ): WSRouteBuilder<TEnv, TCtx & TAdds, TInput, _TErrorKeys, TParent> {
3827
+ if (mw.errors) {
3828
+ for (const k of mw.errors) {
3829
+ this._s.errorKeys.add(k);
3830
+ }
3831
+ }
3832
+ return new WSRouteBuilder<TEnv, TCtx & TAdds, TInput, _TErrorKeys, TParent>(
3833
+ {
3834
+ ...this._s,
3835
+ middlewares: [...this._s.middlewares, mw as RuntimeMiddleware],
3836
+ },
3837
+ );
3838
+ }
3839
+ }
3840
+
3841
+ export function honey<TEnv>(): Honey<TEnv> {
3842
+ return new Honey<TEnv>();
3843
+ }