@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/types.ts ADDED
@@ -0,0 +1,638 @@
1
+ export type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT"
2
+
3
+ /** Overwrite keys in T with keys from U -- used by .use() to support narrowing middleware */
4
+ export type Overwrite<T, U> = Omit<T, keyof U> & U
5
+
6
+ export type WSReadyState = 0 | 1 | 2 | 3
7
+
8
+ /** All HTTP status codes 2xx–5xx as snake_case keys */
9
+ export type StatusKey =
10
+ /* 2xx success */
11
+ | "accepted"
12
+ | "already_reported"
13
+ | "created"
14
+ | "im_used"
15
+ | "multi_status"
16
+ | "no_content"
17
+ | "non_authoritative_information"
18
+ | "ok"
19
+ | "partial_content"
20
+ | "reset_content"
21
+ /* 3xx redirection */
22
+ | "found"
23
+ | "moved_permanently"
24
+ | "multiple_choices"
25
+ | "not_modified"
26
+ | "permanent_redirect"
27
+ | "see_other"
28
+ | "temporary_redirect"
29
+ /* 4xx client error */
30
+ | "bad_request"
31
+ | "conflict"
32
+ | "expectation_failed"
33
+ | "failed_dependency"
34
+ | "forbidden"
35
+ | "gone"
36
+ | "im_a_teapot"
37
+ | "length_required"
38
+ | "locked"
39
+ | "method_not_allowed"
40
+ | "misdirected_request"
41
+ | "not_acceptable"
42
+ | "not_found"
43
+ | "content_too_large"
44
+ | "payment_required"
45
+ | "precondition_failed"
46
+ | "precondition_required"
47
+ | "proxy_authentication_required"
48
+ | "range_not_satisfiable"
49
+ | "request_header_fields_too_large"
50
+ | "request_timeout"
51
+ | "too_early"
52
+ | "too_many_requests"
53
+ | "unauthorized"
54
+ | "unavailable_for_legal_reasons"
55
+ | "unprocessable_entity"
56
+ | "unsupported_media_type"
57
+ | "upgrade_required"
58
+ | "uri_too_long"
59
+ /* 5xx server error */
60
+ | "bad_gateway"
61
+ | "gateway_timeout"
62
+ | "http_version_not_supported"
63
+ | "insufficient_storage"
64
+ | "internal_server_error"
65
+ | "loop_detected"
66
+ | "network_authentication_required"
67
+ | "not_extended"
68
+ | "not_implemented"
69
+ | "service_unavailable"
70
+ | "variant_also_negotiates"
71
+
72
+ export const statusKeyToCode: Record<StatusKey, number> = {
73
+ accepted: 202,
74
+ already_reported: 208,
75
+ bad_gateway: 502,
76
+ bad_request: 400,
77
+ conflict: 409,
78
+ content_too_large: 413,
79
+ created: 201,
80
+ expectation_failed: 417,
81
+ failed_dependency: 424,
82
+ forbidden: 403,
83
+ found: 302,
84
+ gateway_timeout: 504,
85
+ gone: 410,
86
+ http_version_not_supported: 505,
87
+ im_a_teapot: 418,
88
+ im_used: 226,
89
+ insufficient_storage: 507,
90
+ internal_server_error: 500,
91
+ length_required: 411,
92
+ locked: 423,
93
+ loop_detected: 508,
94
+ method_not_allowed: 405,
95
+ misdirected_request: 421,
96
+ moved_permanently: 301,
97
+ multi_status: 207,
98
+ multiple_choices: 300,
99
+ network_authentication_required: 511,
100
+ no_content: 204,
101
+ non_authoritative_information: 203,
102
+ not_acceptable: 406,
103
+ not_extended: 510,
104
+ not_found: 404,
105
+ not_implemented: 501,
106
+ not_modified: 304,
107
+ ok: 200,
108
+ partial_content: 206,
109
+ payment_required: 402,
110
+ permanent_redirect: 308,
111
+ precondition_failed: 412,
112
+ precondition_required: 428,
113
+ proxy_authentication_required: 407,
114
+ range_not_satisfiable: 416,
115
+ request_header_fields_too_large: 431,
116
+ request_timeout: 408,
117
+ reset_content: 205,
118
+ see_other: 303,
119
+ service_unavailable: 503,
120
+ temporary_redirect: 307,
121
+ too_early: 425,
122
+ too_many_requests: 429,
123
+ unauthorized: 401,
124
+ unavailable_for_legal_reasons: 451,
125
+ unprocessable_entity: 422,
126
+ unsupported_media_type: 415,
127
+ upgrade_required: 426,
128
+ uri_too_long: 414,
129
+ variant_also_negotiates: 506,
130
+ }
131
+
132
+ export const codeToStatusKey: Record<number, StatusKey> = Object.fromEntries(
133
+ Object.entries(statusKeyToCode).map(([k, v]) => [v, k]),
134
+ ) as Record<number, StatusKey>
135
+
136
+ /** Framework error keys — centralized to prevent typo-induced silent failures */
137
+ export const EK = {
138
+ bad_gateway: "bad_gateway",
139
+ content_too_large: "content_too_large",
140
+ forbidden: "forbidden",
141
+ gateway_timeout: "gateway_timeout",
142
+ internal_server_error: "internal_server_error",
143
+ method_not_allowed: "method_not_allowed",
144
+ not_found: "not_found",
145
+ output_content_type_mismatch: "output_content_type_mismatch",
146
+ output_validation_failed: "output_validation_failed",
147
+ request_timeout: "request_timeout",
148
+ too_many_requests: "too_many_requests",
149
+ unsupported_media_type: "unsupported_media_type",
150
+ validation_failed: "validation_failed",
151
+ } as const
152
+
153
+ /** Framework status keys — centralized subset of StatusKey used internally */
154
+ export const SK = {
155
+ bad_gateway: "bad_gateway",
156
+ bad_request: "bad_request",
157
+ content_too_large: "content_too_large",
158
+ forbidden: "forbidden",
159
+ gateway_timeout: "gateway_timeout",
160
+ internal_server_error: "internal_server_error",
161
+ method_not_allowed: "method_not_allowed",
162
+ not_found: "not_found",
163
+ unprocessable_entity: "unprocessable_entity",
164
+ unsupported_media_type: "unsupported_media_type",
165
+ } as const satisfies Record<string, StatusKey>
166
+
167
+ export type FieldError = {
168
+ error_key: string
169
+ message: string
170
+ path: string
171
+ }
172
+
173
+ /* shared frozen empty objects — avoid per-call {} allocations */
174
+ export const EMPTY_OBJ: Record<string, never> = Object.freeze(Object.create(null)) as Record<
175
+ string,
176
+ never
177
+ >
178
+ export const EMPTY_FIELDS: Record<string, FieldError[]> = EMPTY_OBJ as Record<string, FieldError[]>
179
+
180
+ export type NormalizedIssue = {
181
+ code: string
182
+ message: string
183
+ meta: {
184
+ expected?: string
185
+ field?: string
186
+ max?: number
187
+ min?: number
188
+ multipleOf?: number
189
+ received?: string
190
+ }
191
+ path: PropertyKey[]
192
+ }
193
+
194
+ export type SchemaDefinition = {
195
+ input: unknown
196
+ output: unknown
197
+ }
198
+
199
+ /**
200
+ * Minimal Standard Schema interface (StandardSchemaV1).
201
+ * Compatible with Zod, Valibot, ArkType, etc.
202
+ */
203
+ export type StandardSchemaLike = {
204
+ readonly "~standard": {
205
+ readonly types?: { readonly input: unknown; readonly output: unknown } | undefined
206
+ readonly validate: (value: unknown) =>
207
+ | {
208
+ readonly issues?: ReadonlyArray<StandardSchemaIssue> | undefined
209
+ readonly value?: unknown
210
+ }
211
+ | Promise<{
212
+ readonly issues?: ReadonlyArray<StandardSchemaIssue> | undefined
213
+ readonly value?: unknown
214
+ }>
215
+ readonly vendor: string
216
+ readonly version: number
217
+ }
218
+ }
219
+
220
+ export type StandardSchemaIssue = {
221
+ readonly message: string
222
+ readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined
223
+ }
224
+
225
+ /** Extract the output type from a Standard Schema compatible schema */
226
+ export type InferOutput<T> = T extends {
227
+ readonly "~standard": { readonly types?: infer TTypes }
228
+ }
229
+ ? NonNullable<TTypes> extends { readonly output: infer O }
230
+ ? O
231
+ : unknown
232
+ : unknown
233
+
234
+ /** Map input schema definitions to their inferred output types, dropping undefined keys */
235
+ export type InferInputMap<T> = {
236
+ [K in keyof T as T[K] extends undefined | null ? never : K]-?: InferOutput<
237
+ UnwrapInputSchema<NonNullable<T[K]>>
238
+ >
239
+ }
240
+
241
+ /**
242
+ * Extract param names from a route path string.
243
+ * "/users/:id/posts/:postId" → "id" | "postId"
244
+ * Handles optional params (:id?) and wildcards (*name)
245
+ */
246
+ export type ParamKeys<Path extends string> = Path extends `${string}:${infer Param}/${infer Rest}`
247
+ ? ParamKey<Param> | ParamKeys<`/${Rest}`>
248
+ : Path extends `${string}:${infer Param}`
249
+ ? ParamKey<Param>
250
+ : Path extends `${string}*${infer Name}`
251
+ ? Name extends ""
252
+ ? "*"
253
+ : Name
254
+ : never
255
+
256
+ type ParamKey<T extends string> = T extends `${infer Name}?` ? Name : T
257
+
258
+ /** Build a params record from a route path string */
259
+ export type ParamsFromPath<Path extends string> = [ParamKeys<Path>] extends [never]
260
+ ? Record<string, string>
261
+ : { [K in ParamKeys<Path>]: string }
262
+
263
+ /** Tagged schema wrapper for runtime behavior control */
264
+ type TaggedSchema = {
265
+ readonly _tag: "readableStream"
266
+ readonly schema: StandardSchemaLike
267
+ }
268
+
269
+ /** A schema entry: plain StandardSchema or wrapped with readableStream() */
270
+ export type InputSchemaEntry = StandardSchemaLike | TaggedSchema
271
+
272
+ /** Unwrap tagged schemas to get the inner StandardSchemaLike for type inference */
273
+ type UnwrapInputSchema<T> = T extends {
274
+ readonly _tag: string
275
+ readonly schema: infer S
276
+ }
277
+ ? S
278
+ : T
279
+
280
+ /** Non-body input sources — combine freely */
281
+ type InputNonBody = {
282
+ cookies?: InputSchemaEntry
283
+ headers?: InputSchemaEntry
284
+ params?: InputSchemaEntry
285
+ search?: InputSchemaEntry
286
+ }
287
+
288
+ /** Input schema definition — json and form are mutually exclusive (one Content-Type per request) */
289
+ export type InputSchemasDef =
290
+ | (InputNonBody & { form?: InputSchemaEntry; json?: never })
291
+ | (InputNonBody & { form?: never; json?: InputSchemaEntry })
292
+
293
+ /** 2xx-only status keys for output declarations */
294
+ export type SuccessStatusKey =
295
+ | "accepted"
296
+ | "already_reported"
297
+ | "created"
298
+ | "im_used"
299
+ | "multi_status"
300
+ | "no_content"
301
+ | "non_authoritative_information"
302
+ | "ok"
303
+ | "partial_content"
304
+ | "reset_content"
305
+
306
+ /** Status-key → schema map for a single content type (2xx only) */
307
+ type StatusSchemaMap = Partial<Record<SuccessStatusKey, StandardSchemaLike>>
308
+
309
+ /** 3xx redirect status keys (excludes not_modified — cache, not redirect) */
310
+ export type RedirectStatusKey =
311
+ | "found"
312
+ | "moved_permanently"
313
+ | "multiple_choices"
314
+ | "permanent_redirect"
315
+ | "see_other"
316
+ | "temporary_redirect"
317
+
318
+ /** Redirect declaration map — no body schema, just status keys */
319
+ type RedirectSchemaMap = Partial<Record<RedirectStatusKey, true>>
320
+
321
+ /** Output schema definition — maps content-type → status-key → schema */
322
+ export type OutputSchemaDef = {
323
+ "application/cbor"?: StatusSchemaMap
324
+ "application/json"?: StatusSchemaMap
325
+ "application/msgpack"?: StatusSchemaMap
326
+ "application/octet-stream"?: StatusSchemaMap
327
+ "application/pdf"?: StatusSchemaMap
328
+ "application/xml"?: StatusSchemaMap
329
+ "redirect"?: RedirectSchemaMap
330
+ "text/csv"?: StatusSchemaMap
331
+ "text/event-stream"?: StatusSchemaMap
332
+ "text/html"?: StatusSchemaMap
333
+ "text/plain"?: StatusSchemaMap
334
+ }
335
+
336
+ /** Maps content-type strings to HoneyRes method names */
337
+ export type ContentTypeMethodMap = {
338
+ "application/json": "json"
339
+ "application/octet-stream": "binary"
340
+ "application/xml": "xml"
341
+ "text/csv": "csv"
342
+ "text/event-stream": "sse"
343
+ "text/html": "html"
344
+ "text/plain": "text"
345
+ }
346
+
347
+ /** Methods always available on ctx.res regardless of output declaration */
348
+ export type UniversalResMethods = "noContent" | "raw" | "redirect" | "stream"
349
+
350
+ /** Derive allowed response methods from declared output content types */
351
+ export type AllowedResMethods<TOutput> = {
352
+ [CT in keyof ContentTypeMethodMap]: CT extends keyof TOutput ? ContentTypeMethodMap[CT] : never
353
+ }[keyof ContentTypeMethodMap]
354
+
355
+ /** Extract schemas for any content type from an OutputSchemaDef */
356
+ export type ExtractSchemas<T, CT extends string> = T extends {
357
+ [K in CT]: infer S extends Record<string, unknown>
358
+ }
359
+ ? S
360
+ : never
361
+
362
+ /** Extract JSON output schemas from an OutputSchemaDef */
363
+ export type ExtractJsonSchemas<T> = ExtractSchemas<T, "application/json">
364
+
365
+ /** Infer output types from a JSON output schema map */
366
+ export type InferJsonOutputMap<T extends Record<string, StandardSchemaLike>> = {
367
+ [K in keyof T]: InferOutput<T[K]>
368
+ }
369
+
370
+ /** OpenAPI operation-level metadata (also serves as built-in meta base) */
371
+ export type OpenApiMeta = {
372
+ internal?: boolean
373
+ deprecated?: boolean
374
+ description?: string
375
+ invalidate?: readonly string[] | null
376
+ mcp?: boolean
377
+ operationId?: string
378
+ security?: Array<Record<string, string[]>> | string | string[]
379
+ summary?: string
380
+ tags?: string | string[]
381
+ }
382
+
383
+ /** Built-in meta shape — always available on .meta() */
384
+ export type DefaultMeta = OpenApiMeta
385
+
386
+ /** Augmented by generated code per-app to narrow invalidate selectors */
387
+ export interface HoneyCodegen {}
388
+
389
+ type ResolveSelector = HoneyCodegen extends { routeSelector: infer R }
390
+ ? R extends string ? R : string
391
+ : string
392
+
393
+ /** Utility: merges built-in meta, custom meta T, and type-safe invalidate from codegen */
394
+ export type HoneyMeta<T = {}> = DefaultMeta & T & {
395
+ invalidate?: readonly ResolveSelector[] | null
396
+ }
397
+
398
+ /** Accumulated route schema entry for RPC client generation */
399
+ export type RouteRecord<
400
+ TMethod extends string,
401
+ TPath extends string,
402
+ TInput,
403
+ TOutput,
404
+ TCtx = unknown,
405
+ TErrors extends string = never,
406
+ TErrorsByStatus = never,
407
+ > = {
408
+ [P in TPath]: {
409
+ [M in Lowercase<TMethod>]: {
410
+ ctx: TCtx
411
+ errors: TErrors
412
+ errorsByStatus: TErrorsByStatus
413
+ input: TInput
414
+ output: TOutput
415
+ }
416
+ }
417
+ }
418
+
419
+ /** Merge a new route record into accumulated routes via flat intersection */
420
+ export type MergeRoute<
421
+ TRoutes,
422
+ TPath extends string,
423
+ TMethod extends string,
424
+ TInput,
425
+ TOutput,
426
+ TCtx,
427
+ TMeta = {},
428
+ TErrors extends string = never,
429
+ TErrorsByStatus = never,
430
+ > = TRoutes & {
431
+ [P in TPath]: {
432
+ [M in Lowercase<TMethod>]: {
433
+ ctx: TCtx
434
+ errors: TErrors
435
+ errorsByStatus: TErrorsByStatus
436
+ input: TInput
437
+ meta: TMeta
438
+ output: TOutput
439
+ }
440
+ }
441
+ }
442
+
443
+ /** Extract accumulated routes from a Honey instance type */
444
+ export type InferRoutes<T> = T extends { readonly $routes: infer R } ? R : never
445
+
446
+ /** Extract accumulated middleware context. `res` is omitted so handler ctx is assignable into services. Use InferRouteCtx for the route's constrained `res`. */
447
+ export type InferCtx<T> = T extends { readonly $ctx: infer TCtx } ? Omit<TCtx, "res"> : never
448
+
449
+ /** Extract environment type (TEnv) from a Honey instance */
450
+ export type InferEnv<T> = T extends { readonly $env: infer TEnv } ? TEnv : never
451
+
452
+ /** All HTTP methods used across all routes */
453
+ export type InferMethods<T> =
454
+ InferRoutes<T> extends infer R ? { [P in keyof R]: keyof R[P] & string }[keyof R] : never
455
+
456
+ /** Available route paths registered on a Honey instance */
457
+ export type InferRoutePaths<T> = keyof InferRoutes<T> & string
458
+
459
+ /** Available methods for a given path ("get", "post", etc.) */
460
+ export type InferRouteMethods<
461
+ T,
462
+ TPath extends InferRoutePaths<T>,
463
+ > = InferRoutes<T>[TPath] extends infer M ? keyof M & string : never
464
+
465
+ /** Lookup a specific route's record from the accumulated route map */
466
+ type RouteLookup<T, TPath extends string, TMethod extends string> =
467
+ InferRoutes<T> extends infer R
468
+ ? TPath extends keyof R
469
+ ? R[TPath] extends infer Methods
470
+ ? TMethod extends keyof Methods
471
+ ? Methods[TMethod]
472
+ : never
473
+ : never
474
+ : never
475
+ : never
476
+
477
+ /** Extract the meta type generic (TMeta) from a Honey instance */
478
+ export type InferMeta<T> = T extends { readonly $meta: infer M } ? M : never
479
+
480
+ /** Extract the full handler context type for a specific route */
481
+ export type InferRouteCtx<
482
+ T,
483
+ TPath extends InferRoutePaths<T>,
484
+ TMethod extends InferRouteMethods<T, TPath>,
485
+ > = RouteLookup<T, TPath, TMethod> extends { ctx: infer C } ? C : never
486
+
487
+ /** Extract input type for a specific route */
488
+ export type InferRouteInput<
489
+ T,
490
+ TPath extends InferRoutePaths<T>,
491
+ TMethod extends InferRouteMethods<T, TPath>,
492
+ > = RouteLookup<T, TPath, TMethod> extends { input: infer I } ? I : never
493
+
494
+ /** Extract meta type for a specific route */
495
+ export type InferRouteMeta<
496
+ T,
497
+ TPath extends InferRoutePaths<T>,
498
+ TMethod extends InferRouteMethods<T, TPath>,
499
+ > = RouteLookup<T, TPath, TMethod> extends { meta: infer M } ? M : never
500
+
501
+ /** Extract output type for a specific route */
502
+ export type InferRouteOutput<
503
+ T,
504
+ TPath extends InferRoutePaths<T>,
505
+ TMethod extends InferRouteMethods<T, TPath>,
506
+ > = RouteLookup<T, TPath, TMethod> extends { output: infer O } ? O : never
507
+
508
+ /** Extract error keys for a specific route */
509
+ export type InferRouteErrors<
510
+ T,
511
+ TPath extends InferRoutePaths<T>,
512
+ TMethod extends InferRouteMethods<T, TPath>,
513
+ > = RouteLookup<T, TPath, TMethod> extends { errors: infer E } ? E : never
514
+
515
+ /** Extract the error factory type from a Honey instance */
516
+ export type InferErrorFactory<T> = T extends { readonly $errorFactory: infer F } ? F : never
517
+
518
+ /**
519
+ * Compute errorsByStatus from error factory + declared error keys.
520
+ * Maps status codes to body shapes for client-side discrimination.
521
+ */
522
+ export type ComputeErrorsByStatus<
523
+ TFactory,
524
+ TErrorKeys extends string,
525
+ TMetaSymbol extends symbol,
526
+ > = TFactory extends { readonly [K in TMetaSymbol]: infer TMeta }
527
+ ? TMeta extends Record<string, { schema: infer _S; statusKey: infer _SK }>
528
+ ? _BuildByStatus<TMeta, TErrorKeys>
529
+ : never
530
+ : never
531
+
532
+ /** Internal: build { [statusCode]: bodyShape } from meta + error keys */
533
+ type _BuildByStatus<
534
+ TMeta extends Record<string, { schema: unknown; statusKey: unknown }>,
535
+ TKeys extends string,
536
+ > = {
537
+ [SK in _UsedStatusKeys<TMeta, TKeys> & StatusKey as StatusKeyToCodeType<SK>]: _ShapesForStatus<
538
+ TMeta,
539
+ TKeys,
540
+ SK
541
+ >
542
+ }
543
+
544
+ /** Internal: collect all statusKeys used by the declared error keys */
545
+ type _UsedStatusKeys<
546
+ TMeta extends Record<string, { schema: unknown; statusKey: unknown }>,
547
+ TKeys extends string,
548
+ > = TKeys extends keyof TMeta ? TMeta[TKeys]["statusKey"] : never
549
+
550
+ /** Internal: union of shapes for all error keys that map to a given statusKey */
551
+ type _ShapesForStatus<
552
+ TMeta extends Record<string, { schema: unknown; statusKey: unknown }>,
553
+ TKeys extends string,
554
+ TSK extends StatusKey,
555
+ > = TKeys extends keyof TMeta
556
+ ? TMeta[TKeys]["statusKey"] extends TSK
557
+ ? TMeta[TKeys]["schema"] extends StandardSchemaLike
558
+ ? InferOutput<TMeta[TKeys]["schema"]>
559
+ : null
560
+ : never
561
+ : never
562
+
563
+ /** Map StatusKey string to numeric status code at the type level */
564
+ type StatusKeyToCodeType<SK extends StatusKey> = SK extends "ok"
565
+ ? 200
566
+ : SK extends "created"
567
+ ? 201
568
+ : SK extends "accepted"
569
+ ? 202
570
+ : SK extends "no_content"
571
+ ? 204
572
+ : SK extends "bad_request"
573
+ ? 400
574
+ : SK extends "unauthorized"
575
+ ? 401
576
+ : SK extends "payment_required"
577
+ ? 402
578
+ : SK extends "forbidden"
579
+ ? 403
580
+ : SK extends "not_found"
581
+ ? 404
582
+ : SK extends "method_not_allowed"
583
+ ? 405
584
+ : SK extends "not_acceptable"
585
+ ? 406
586
+ : SK extends "conflict"
587
+ ? 409
588
+ : SK extends "gone"
589
+ ? 410
590
+ : SK extends "unprocessable_entity"
591
+ ? 422
592
+ : SK extends "too_many_requests"
593
+ ? 429
594
+ : SK extends "internal_server_error"
595
+ ? 500
596
+ : SK extends "not_implemented"
597
+ ? 501
598
+ : SK extends "bad_gateway"
599
+ ? 502
600
+ : SK extends "service_unavailable"
601
+ ? 503
602
+ : SK extends "gateway_timeout"
603
+ ? 504
604
+ : number
605
+
606
+ /** Extract the base path type from a Honey instance */
607
+ export type InferBasePath<T> = T extends { readonly $basePath: infer P } ? P : never
608
+
609
+ /** Merge two path segments — "/" is identity */
610
+ export type MergePath<A extends string, B extends string> = A extends "/"
611
+ ? B
612
+ : B extends "/"
613
+ ? A
614
+ : `${A}${B}`
615
+
616
+ /* ── Tap types ── */
617
+
618
+ /** Queued side-effect emitted by c.tap() inside a handler */
619
+ export type PendingTap = { key: string; payload: unknown }
620
+
621
+ /** Handler registered via app.tap(key, handler) — receives context + payload */
622
+ export type TapHandler<TEnv = unknown> = (
623
+ ctx: TapContext<TEnv>,
624
+ payload: unknown,
625
+ ) => void | Promise<void>
626
+
627
+ /** Subset of HoneyContext exposed to tap handlers — no res, no recursive tap */
628
+ export type TapContext<TEnv = unknown> = {
629
+ readonly background: (p: Promise<unknown>) => void
630
+ readonly env: TEnv
631
+ readonly headers: Record<string, string>
632
+ readonly meta: Record<string, unknown>
633
+ readonly params: Record<string, string>
634
+ readonly path: string
635
+ readonly req: Request
636
+ readonly routePattern: string
637
+ readonly search: Record<string, string>
638
+ }