@lovrozagar/honey 0.1.0 → 0.2.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 (263) hide show
  1. package/README.md +1596 -6
  2. package/dist/accepts.d.ts +6 -0
  3. package/dist/accepts.d.ts.map +1 -0
  4. package/dist/body-limit.d.ts +22 -0
  5. package/dist/body-limit.d.ts.map +1 -0
  6. package/dist/build/index.d.ts +42 -0
  7. package/dist/build/index.d.ts.map +1 -0
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/client/error.d.ts +14 -0
  11. package/dist/client/error.d.ts.map +1 -0
  12. package/dist/client/http.d.ts +88 -0
  13. package/dist/client/http.d.ts.map +1 -0
  14. package/dist/client/index.d.ts +22 -0
  15. package/dist/client/index.d.ts.map +1 -0
  16. package/dist/client/sdk.d.ts +34 -0
  17. package/dist/client/sdk.d.ts.map +1 -0
  18. package/dist/client/sse.d.ts +10 -0
  19. package/dist/client/sse.d.ts.map +1 -0
  20. package/dist/client/types.d.ts +124 -0
  21. package/dist/client/types.d.ts.map +1 -0
  22. package/dist/client/ws.d.ts +15 -0
  23. package/dist/client/ws.d.ts.map +1 -0
  24. package/dist/client-mcp/server.d.ts +10 -0
  25. package/dist/client-mcp/server.d.ts.map +1 -0
  26. package/dist/client-mcp/types.d.ts +16 -0
  27. package/dist/client-mcp/types.d.ts.map +1 -0
  28. package/dist/cloudflare/realtime.d.ts +30 -0
  29. package/dist/cloudflare/realtime.d.ts.map +1 -0
  30. package/dist/codegen-go-cli.d.ts +22 -0
  31. package/dist/codegen-go-cli.d.ts.map +1 -0
  32. package/dist/codegen-go.d.ts +29 -0
  33. package/dist/codegen-go.d.ts.map +1 -0
  34. package/dist/codegen-hash.d.ts +3 -0
  35. package/dist/codegen-hash.d.ts.map +1 -0
  36. package/dist/codegen-ir.d.ts +123 -0
  37. package/dist/codegen-ir.d.ts.map +1 -0
  38. package/dist/codegen-loaders.d.ts +12 -0
  39. package/dist/codegen-loaders.d.ts.map +1 -0
  40. package/dist/codegen-mcp.d.ts +13 -0
  41. package/dist/codegen-mcp.d.ts.map +1 -0
  42. package/dist/codegen-python.d.ts +21 -0
  43. package/dist/codegen-python.d.ts.map +1 -0
  44. package/dist/codegen-rust.d.ts +18 -0
  45. package/dist/codegen-rust.d.ts.map +1 -0
  46. package/dist/codegen-sanitize.d.ts +3 -0
  47. package/dist/codegen-sanitize.d.ts.map +1 -0
  48. package/dist/codegen-schema-naming.d.ts +22 -0
  49. package/dist/codegen-schema-naming.d.ts.map +1 -0
  50. package/dist/codegen.d.ts +178 -0
  51. package/dist/codegen.d.ts.map +1 -0
  52. package/dist/context.d.ts +56 -0
  53. package/dist/context.d.ts.map +1 -0
  54. package/dist/cookie-sign.d.ts +3 -0
  55. package/dist/cookie-sign.d.ts.map +1 -0
  56. package/dist/cookie.d.ts +12 -0
  57. package/dist/cookie.d.ts.map +1 -0
  58. package/dist/cors.d.ts +13 -0
  59. package/dist/cors.d.ts.map +1 -0
  60. package/dist/crypto.d.ts +2 -0
  61. package/dist/crypto.d.ts.map +1 -0
  62. package/dist/csrf.d.ts +9 -0
  63. package/dist/csrf.d.ts.map +1 -0
  64. package/dist/curl-logger.d.ts +33 -0
  65. package/dist/curl-logger.d.ts.map +1 -0
  66. package/dist/detect-runtime.d.ts +16 -0
  67. package/dist/detect-runtime.d.ts.map +1 -0
  68. package/dist/error.d.ts +21 -0
  69. package/dist/error.d.ts.map +1 -0
  70. package/dist/errors.d.ts +40 -0
  71. package/dist/errors.d.ts.map +1 -0
  72. package/dist/etag.d.ts +9 -0
  73. package/dist/etag.d.ts.map +1 -0
  74. package/dist/feature-load.d.ts +3 -0
  75. package/dist/feature-load.d.ts.map +1 -0
  76. package/dist/go-type-emitter.d.ts +33 -0
  77. package/dist/go-type-emitter.d.ts.map +1 -0
  78. package/dist/honey-response.d.ts +82 -0
  79. package/dist/honey-response.d.ts.map +1 -0
  80. package/dist/i18n-slot.d.ts +7 -0
  81. package/dist/i18n-slot.d.ts.map +1 -0
  82. package/dist/i18n.d.ts +26 -0
  83. package/dist/i18n.d.ts.map +1 -0
  84. package/dist/index.d.ts +470 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/init.d.ts +7 -0
  87. package/dist/init.d.ts.map +1 -0
  88. package/dist/input.d.ts +6 -0
  89. package/dist/input.d.ts.map +1 -0
  90. package/dist/ip-restrict.d.ts +12 -0
  91. package/dist/ip-restrict.d.ts.map +1 -0
  92. package/dist/logger.d.ts +45 -0
  93. package/dist/logger.d.ts.map +1 -0
  94. package/dist/meta-spec.d.ts +81 -0
  95. package/dist/meta-spec.d.ts.map +1 -0
  96. package/dist/middleware.d.ts +60 -0
  97. package/dist/middleware.d.ts.map +1 -0
  98. package/dist/node-request.d.ts +59 -0
  99. package/dist/node-request.d.ts.map +1 -0
  100. package/dist/node.d.ts +13 -0
  101. package/dist/node.d.ts.map +1 -0
  102. package/dist/openapi/register.d.ts +3 -0
  103. package/dist/openapi/register.d.ts.map +1 -0
  104. package/dist/openapi/scalar.d.ts +12 -0
  105. package/dist/openapi/scalar.d.ts.map +1 -0
  106. package/dist/openapi/spec-factory.d.ts +31 -0
  107. package/dist/openapi/spec-factory.d.ts.map +1 -0
  108. package/dist/openapi/spec.d.ts +19 -0
  109. package/dist/openapi/spec.d.ts.map +1 -0
  110. package/dist/openapi/swagger.d.ts +12 -0
  111. package/dist/openapi/swagger.d.ts.map +1 -0
  112. package/dist/plugin.d.ts +167 -0
  113. package/dist/plugin.d.ts.map +1 -0
  114. package/dist/powered-by.d.ts +7 -0
  115. package/dist/powered-by.d.ts.map +1 -0
  116. package/dist/pretty-json.d.ts +10 -0
  117. package/dist/pretty-json.d.ts.map +1 -0
  118. package/dist/proxy.d.ts +51 -0
  119. package/dist/proxy.d.ts.map +1 -0
  120. package/dist/python-type-emitter.d.ts +11 -0
  121. package/dist/python-type-emitter.d.ts.map +1 -0
  122. package/dist/realtime/buffer.d.ts +18 -0
  123. package/dist/realtime/buffer.d.ts.map +1 -0
  124. package/dist/realtime/bus.d.ts +11 -0
  125. package/dist/realtime/bus.d.ts.map +1 -0
  126. package/dist/realtime/longpoll.d.ts +23 -0
  127. package/dist/realtime/longpoll.d.ts.map +1 -0
  128. package/dist/realtime/protocol.d.ts +50 -0
  129. package/dist/realtime/protocol.d.ts.map +1 -0
  130. package/dist/realtime/route.d.ts +42 -0
  131. package/dist/realtime/route.d.ts.map +1 -0
  132. package/dist/realtime/runtime.d.ts +74 -0
  133. package/dist/realtime/runtime.d.ts.map +1 -0
  134. package/dist/request-id.d.ts +12 -0
  135. package/dist/request-id.d.ts.map +1 -0
  136. package/dist/request-to-curl.d.ts +9 -0
  137. package/dist/request-to-curl.d.ts.map +1 -0
  138. package/dist/response.d.ts +57 -0
  139. package/dist/response.d.ts.map +1 -0
  140. package/dist/rust-type-emitter.d.ts +32 -0
  141. package/dist/rust-type-emitter.d.ts.map +1 -0
  142. package/dist/secure-headers.d.ts +16 -0
  143. package/dist/secure-headers.d.ts.map +1 -0
  144. package/dist/serve-register.d.ts +2 -0
  145. package/dist/serve-register.d.ts.map +1 -0
  146. package/dist/serve-slot.d.ts +6 -0
  147. package/dist/serve-slot.d.ts.map +1 -0
  148. package/dist/serve.d.ts +21 -0
  149. package/dist/serve.d.ts.map +1 -0
  150. package/dist/server-timing.d.ts +9 -0
  151. package/dist/server-timing.d.ts.map +1 -0
  152. package/dist/static.d.ts +12 -0
  153. package/dist/static.d.ts.map +1 -0
  154. package/dist/telemetry/otel.d.ts +59 -0
  155. package/dist/telemetry/otel.d.ts.map +1 -0
  156. package/dist/testing.d.ts +24 -0
  157. package/dist/testing.d.ts.map +1 -0
  158. package/dist/timeout.d.ts +7 -0
  159. package/dist/timeout.d.ts.map +1 -0
  160. package/dist/tree.d.ts +90 -0
  161. package/dist/tree.d.ts.map +1 -0
  162. package/dist/ts-type-emitter.d.ts +4 -0
  163. package/dist/ts-type-emitter.d.ts.map +1 -0
  164. package/dist/type-emitter.d.ts +8 -0
  165. package/dist/type-emitter.d.ts.map +1 -0
  166. package/dist/type-extractor.d.ts +39 -0
  167. package/dist/type-extractor.d.ts.map +1 -0
  168. package/dist/types.d.ts +451 -0
  169. package/dist/types.d.ts.map +1 -0
  170. package/dist/validation.d.ts +18 -0
  171. package/dist/validation.d.ts.map +1 -0
  172. package/dist/ws/bun.d.ts +28 -0
  173. package/dist/ws/bun.d.ts.map +1 -0
  174. package/dist/ws/cloudflare.d.ts +55 -0
  175. package/dist/ws/cloudflare.d.ts.map +1 -0
  176. package/dist/ws/deno.d.ts +13 -0
  177. package/dist/ws/deno.d.ts.map +1 -0
  178. package/dist/ws/node.d.ts +10 -0
  179. package/dist/ws/node.d.ts.map +1 -0
  180. package/dist/yaml.d.ts +4 -0
  181. package/dist/yaml.d.ts.map +1 -0
  182. package/docs/api-ref/go.md +82 -82
  183. package/docs/api-ref/python.md +82 -82
  184. package/docs/api-ref/rust.md +82 -82
  185. package/docs/api-ref/typescript.md +82 -82
  186. package/docs/invalidate-spec.md +13 -13
  187. package/docs/meta-spec.md +513 -0
  188. package/docs/packaging.md +85 -0
  189. package/docs/sdk.md +87 -73
  190. package/package.json +195 -51
  191. package/src/body-limit.ts +2 -1
  192. package/src/build/index.ts +2 -9
  193. package/src/cli.ts +2 -3
  194. package/src/client/http.ts +204 -223
  195. package/src/client/index.ts +6 -14
  196. package/src/client/sdk.ts +24 -17
  197. package/src/client/types.ts +4 -9
  198. package/src/client/ws.ts +3 -5
  199. package/src/client-mcp/server.ts +1 -4
  200. package/src/cloudflare/realtime.ts +8 -10
  201. package/src/codegen-go-cli.ts +74 -67
  202. package/src/codegen-go.ts +71 -51
  203. package/src/codegen-ir.ts +16 -27
  204. package/src/codegen-loaders.ts +5 -8
  205. package/src/codegen-mcp.ts +9 -27
  206. package/src/codegen-python.ts +47 -27
  207. package/src/codegen-rust.ts +110 -66
  208. package/src/codegen-sanitize.ts +1 -7
  209. package/src/codegen-schema-naming.ts +8 -2
  210. package/src/codegen.ts +299 -196
  211. package/src/context.ts +91 -95
  212. package/src/cookie-sign.ts +5 -13
  213. package/src/cookie.ts +1 -2
  214. package/src/cors.ts +4 -6
  215. package/src/crypto.ts +1 -4
  216. package/src/csrf.ts +3 -3
  217. package/src/curl-logger.ts +14 -32
  218. package/src/detect-runtime.ts +1 -3
  219. package/src/errors.ts +1 -3
  220. package/src/etag.ts +3 -4
  221. package/src/go-type-emitter.ts +45 -40
  222. package/src/honey-response.ts +1 -3
  223. package/src/i18n.ts +3 -12
  224. package/src/index.ts +1168 -1412
  225. package/src/ip-restrict.ts +2 -1
  226. package/src/logger.ts +13 -13
  227. package/src/meta-spec.ts +0 -0
  228. package/src/middleware.ts +111 -79
  229. package/src/node-request.ts +2 -1
  230. package/src/node.ts +9 -10
  231. package/src/openapi/register.ts +1 -2
  232. package/src/openapi/scalar.ts +9 -9
  233. package/src/openapi/spec-factory.ts +2 -2
  234. package/src/openapi/spec.ts +26 -19
  235. package/src/openapi/swagger.ts +9 -9
  236. package/src/plugin.ts +226 -256
  237. package/src/powered-by.ts +2 -1
  238. package/src/pretty-json.ts +2 -1
  239. package/src/proxy.ts +1 -3
  240. package/src/python-type-emitter.ts +37 -12
  241. package/src/realtime/route.ts +1 -1
  242. package/src/realtime/runtime.ts +9 -12
  243. package/src/request-id.ts +3 -4
  244. package/src/request-to-curl.ts +1 -4
  245. package/src/response.ts +150 -199
  246. package/src/rust-type-emitter.ts +66 -25
  247. package/src/secure-headers.ts +2 -1
  248. package/src/serve.ts +3 -12
  249. package/src/server-timing.ts +2 -1
  250. package/src/static.ts +3 -4
  251. package/src/telemetry/otel.ts +1 -6
  252. package/src/testing.ts +2 -11
  253. package/src/timeout.ts +2 -1
  254. package/src/tree.ts +12 -27
  255. package/src/ts-type-emitter.ts +71 -15
  256. package/src/type-emitter.ts +226 -237
  257. package/src/type-extractor.ts +260 -334
  258. package/src/types.ts +168 -68
  259. package/src/validation.ts +7 -30
  260. package/src/ws/bun.ts +2 -9
  261. package/src/ws/cloudflare.ts +13 -16
  262. package/src/ws/deno.ts +31 -40
  263. package/src/ws/node.ts +4 -19
package/src/codegen.ts CHANGED
@@ -1,20 +1,19 @@
1
- import {
2
- deriveErrorEnvelopeName,
3
- deriveSchemaName,
4
- isErrorEnvelope,
5
- shortHash,
6
- } from "./codegen-schema-naming.ts"
1
+ import { deriveErrorEnvelopeName, deriveSchemaName, isErrorEnvelope, shortHash } from "./codegen-schema-naming.ts"
7
2
  import type { SchemaNameContext } from "./codegen-schema-naming.ts"
8
- import {
9
- effectJsonSchemaFn,
10
- loadEffectJsonSchema,
11
- loadToJSONSchema,
12
- toJSONSchemaFn,
13
- } from "./codegen-loaders.ts"
3
+ import { effectJsonSchemaFn, loadEffectJsonSchema, loadToJSONSchema, toJSONSchemaFn } from "./codegen-loaders.ts"
14
4
  import { sanitizeZodJsonSchema } from "./codegen-sanitize.ts"
15
5
  import { methodsOf, namespacesOf, schemaToIR, toIR } from "./codegen-ir.ts"
16
6
  import type { IRNamespace } from "./codegen-ir.ts"
17
7
  import type { HoneyError } from "./error.ts"
8
+ import {
9
+ applyMetaSpec,
10
+ compileMetaSpec,
11
+ type CompiledMetaSpec,
12
+ MetaSpecCollector,
13
+ resolveProfile,
14
+ type SchemaMetaHit,
15
+ type SchemaMetaLookup,
16
+ } from "./meta-spec.ts"
18
17
  import { ERROR_META } from "./errors.ts"
19
18
  import type { ErrorMetaEntry } from "./errors.ts"
20
19
  import type { Honey } from "./index.ts"
@@ -24,6 +23,8 @@ import { emitSchemaType } from "./type-emitter.ts"
24
23
  import type {
25
24
  InputSchemaEntry,
26
25
  InputSchemasDef,
26
+ MetaSpecConfig,
27
+ MetaSpecSchemaSource,
27
28
  OutputSchemaDef,
28
29
  StandardSchemaLike,
29
30
  } from "./types.ts"
@@ -314,12 +315,7 @@ function isMetaInternal(handler: RouteHandler): boolean {
314
315
  return (handler.mt as { internal?: boolean } | null)?.internal === true
315
316
  }
316
317
 
317
- function walkTree(
318
- node: TreeNode,
319
- currentPath: string,
320
- routes: CollectedRoute[],
321
- includeSkipped?: boolean,
322
- ): void {
318
+ function walkTree(node: TreeNode, currentPath: string, routes: CollectedRoute[], includeSkipped?: boolean): void {
323
319
  if (node.m !== null) {
324
320
  for (const [method, handler] of Object.entries(node.m)) {
325
321
  if (handler._skip && !includeSkipped) continue
@@ -362,10 +358,7 @@ function walkWSRoutes(node: TreeNode, currentPath: string, routes: CollectedWSRo
362
358
 
363
359
  type ErrorInfo = { errorKey: string; status: number; statusKey: string }
364
360
 
365
- function resolveErrorInfo(
366
- errorKey: string,
367
- factory: Record<string, () => HoneyError> | null,
368
- ): ErrorInfo {
361
+ function resolveErrorInfo(errorKey: string, factory: Record<string, () => HoneyError> | null): ErrorInfo {
369
362
  if (factory?.[errorKey]) {
370
363
  try {
371
364
  const err = factory[errorKey]()
@@ -380,8 +373,7 @@ function resolveErrorInfo(
380
373
  function getErrorFactory<TEnv, TCtx>(
381
374
  app: Honey<TEnv, TCtx, unknown, unknown, unknown, string, string>,
382
375
  ): Record<string, () => HoneyError> | null {
383
- return (app as unknown as { _errorFactory: Record<string, () => HoneyError> | null })
384
- ._errorFactory
376
+ return (app as unknown as { _errorFactory: Record<string, () => HoneyError> | null })._errorFactory
385
377
  }
386
378
 
387
379
  export const DEFAULT_ERROR_JSON_SCHEMA: Record<string, unknown> = {
@@ -423,9 +415,7 @@ function getCustomErrorSchema<TEnv, TCtx>(
423
415
  return (app as unknown as { _customErrorSchema: StandardSchemaLike | null })._customErrorSchema
424
416
  }
425
417
 
426
- function getErrorMeta(
427
- factory: Record<string, () => HoneyError> | null,
428
- ): Record<string, ErrorMetaEntry> | null {
418
+ function getErrorMeta(factory: Record<string, () => HoneyError> | null): Record<string, ErrorMetaEntry> | null {
429
419
  if (!factory) return null
430
420
  return (factory as Record<symbol, Record<string, ErrorMetaEntry>>)[ERROR_META] ?? null
431
421
  }
@@ -446,25 +436,7 @@ function introspectSchema(schema: StandardSchemaLike): unknown {
446
436
  }
447
437
  }
448
438
 
449
- /* Normalize security shorthand → standard OpenAPI security array */
450
- export function normalizeSecurity(raw: unknown): Record<string, string[]>[] {
451
- if (typeof raw === "string") return [{ [raw]: [] }]
452
- if (!Array.isArray(raw)) return []
453
- if (raw.length === 0) return []
454
- if (raw.every((entry) => typeof entry === "string")) {
455
- return raw.map((entry) => ({ [entry]: [] }))
456
- }
457
- if (raw.every((entry) => Array.isArray(entry))) {
458
- return (raw as string[][]).map((group) =>
459
- Object.fromEntries(group.map((s) => [s, []])),
460
- )
461
- }
462
- if (raw.every((entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry))) {
463
- return raw as Record<string, string[]>[]
464
- }
465
- /* mixed shapes — refuse to guess */
466
- return []
467
- }
439
+ export { normalizeSecurity } from "./meta-spec.ts"
468
440
 
469
441
  /* Strip properties marked with x-internal from an object JSON schema */
470
442
  function stripInternalProps(schema: Record<string, unknown>): Record<string, unknown> {
@@ -486,9 +458,7 @@ function stripInternalProps(schema: Record<string, unknown>): Record<string, unk
486
458
  export function canonicalizeSchema(schema: Record<string, unknown>): string {
487
459
  return JSON.stringify(schema, (_, value) => {
488
460
  if (value && typeof value === "object" && !Array.isArray(value)) {
489
- return Object.fromEntries(
490
- Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)),
491
- )
461
+ return Object.fromEntries(Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)))
492
462
  }
493
463
  return value
494
464
  })
@@ -509,9 +479,7 @@ function isObjectSchema(schema: unknown): schema is Record<string, unknown> {
509
479
  return s.type === "object" || (typeof s.properties === "object" && s.properties !== null)
510
480
  }
511
481
 
512
- function collectSchemaSlots(
513
- paths: Record<string, Record<string, Record<string, unknown>>>,
514
- ): SchemaSlot[] {
482
+ function collectSchemaSlots(paths: Record<string, Record<string, Record<string, unknown>>>): SchemaSlot[] {
515
483
  const slots: SchemaSlot[] = []
516
484
 
517
485
  for (const [pathKey, methods] of Object.entries(paths)) {
@@ -615,8 +583,7 @@ function collectHoistedSchemas(
615
583
  const slotName = slotNames.get(slot)
616
584
  /* Mark slots whose schema is an error envelope — their nested objects (e.g. `fields`)
617
585
  * are env-specific and must not be named after the parent envelope's owner slot. */
618
- if (slotName)
619
- walkNestedObjects(slot.schema, slotName, [], nestedMap, isErrorEnvelope(slot.schema))
586
+ if (slotName) walkNestedObjects(slot.schema, slotName, [], nestedMap, isErrorEnvelope(slot.schema))
620
587
  }
621
588
 
622
589
  const schemas: Record<string, Record<string, unknown>> = {}
@@ -825,8 +792,7 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
825
792
  if (groupSlots.length === 1) {
826
793
  name = deriveSchemaName(firstSlot.context)
827
794
  } else if (isErrorEnvelope(firstSlot.schema)) {
828
- const fallbackStatus =
829
- firstSlot.context.role === "response" ? firstSlot.context.status : undefined
795
+ const fallbackStatus = firstSlot.context.role === "response" ? firstSlot.context.status : undefined
830
796
  name = deriveErrorEnvelopeName(firstSlot.schema, fallbackStatus)
831
797
  } else {
832
798
  name = deriveSchemaName(firstSlot.context)
@@ -852,8 +818,11 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
852
818
  * Error envelope slots (any tier) mark their nested schemas as envelope-owned so
853
819
  * envelope-exclusive nesting (e.g. `fields`) is not hoisted under a per-spec name. */
854
820
  const allTopLevelNames = new Set(slotNames.values())
855
- const { schemas: hoistedSchemas, canonicalToName: hoistedCanonicalToName } =
856
- collectHoistedSchemas(slots, slotNames, allTopLevelNames)
821
+ const { schemas: hoistedSchemas, canonicalToName: hoistedCanonicalToName } = collectHoistedSchemas(
822
+ slots,
823
+ slotNames,
824
+ allTopLevelNames,
825
+ )
857
826
 
858
827
  /* Build components map, rewriting nested fields → $refs where hoisted */
859
828
  const extractedSchemas: Record<string, Record<string, unknown>> = {}
@@ -863,9 +832,7 @@ export function deduplicateSchemas(spec: OpenApiSpec): OpenApiSpec {
863
832
  if (name && !seenNames.has(name)) {
864
833
  seenNames.add(name)
865
834
  extractedSchemas[name] =
866
- hoistedCanonicalToName.size > 0
867
- ? rewriteNestedRefs(slot.schema, hoistedCanonicalToName)
868
- : slot.schema
835
+ hoistedCanonicalToName.size > 0 ? rewriteNestedRefs(slot.schema, hoistedCanonicalToName) : slot.schema
869
836
  }
870
837
  }
871
838
  Object.assign(extractedSchemas, hoistedSchemas)
@@ -894,10 +861,7 @@ export type OpenApiSanitizeOptions = {
894
861
  stripXExtensions?: boolean | string[]
895
862
  }
896
863
 
897
- export function sanitizeOpenApiSpec(
898
- spec: OpenApiSpec,
899
- options: OpenApiSanitizeOptions,
900
- ): OpenApiSpec {
864
+ export function sanitizeOpenApiSpec(spec: OpenApiSpec, options: OpenApiSanitizeOptions): OpenApiSpec {
901
865
  const result = cloneJson(spec)
902
866
 
903
867
  if (options.stripSecuritySchemes?.length && result.components?.securitySchemes) {
@@ -989,8 +953,16 @@ function resolveSchemaDeep(
989
953
  }
990
954
  }
991
955
  /* resolve additionalProperties */
992
- if (schema.additionalProperties && typeof schema.additionalProperties === "object" && !Array.isArray(schema.additionalProperties)) {
993
- schema.additionalProperties = resolveSchema(schema.additionalProperties as Record<string, unknown>, schemas, visited)
956
+ if (
957
+ schema.additionalProperties &&
958
+ typeof schema.additionalProperties === "object" &&
959
+ !Array.isArray(schema.additionalProperties)
960
+ ) {
961
+ schema.additionalProperties = resolveSchema(
962
+ schema.additionalProperties as Record<string, unknown>,
963
+ schemas,
964
+ visited,
965
+ )
994
966
  }
995
967
  return schema
996
968
  }
@@ -1088,7 +1060,11 @@ function applyZodBag(schema: unknown, json: Record<string, unknown>): Record<str
1088
1060
  }
1089
1061
 
1090
1062
  function zodDefOf(schema: unknown): Record<string, unknown> | undefined {
1091
- const s = schema as { _def?: Record<string, unknown>; def?: Record<string, unknown>; _zod?: { def?: Record<string, unknown> } }
1063
+ const s = schema as {
1064
+ _def?: Record<string, unknown>
1065
+ def?: Record<string, unknown>
1066
+ _zod?: { def?: Record<string, unknown> }
1067
+ }
1092
1068
  return s._def ?? s.def ?? s._zod?.def
1093
1069
  }
1094
1070
 
@@ -1242,8 +1218,7 @@ function schemaToJsonSchema(schema: StandardSchemaLike, io: "input" | "output" =
1242
1218
  }
1243
1219
  } catch (err) {
1244
1220
  const msg = err instanceof Error ? err.message : String(err)
1245
- const defType =
1246
- (schema as { _zod?: { def?: { type?: string } } })?._zod?.def?.type ?? "unknown"
1221
+ const defType = (schema as { _zod?: { def?: { type?: string } } })?._zod?.def?.type ?? "unknown"
1247
1222
  console.warn(
1248
1223
  `[honey:codegen] schemaToJsonSchema failed for zod/${defType} (io=${io}): ${msg}. Falling back to metadata-only introspection.`,
1249
1224
  )
@@ -1350,15 +1325,137 @@ export type OpenApiRouteInfo<TMeta = unknown> = {
1350
1325
  path: string
1351
1326
  }
1352
1327
 
1328
+ function getMetaSpecConfig(app: unknown): MetaSpecConfig | null {
1329
+ return (app as { _metaSpec?: MetaSpecConfig | null })._metaSpec ?? null
1330
+ }
1331
+
1332
+ /** Lowest declared 2xx JSON output schema — the "output" schema-meta source */
1333
+ function primaryOutputSchema(handler: RouteHandler): StandardSchemaLike | null {
1334
+ const json = handler.os?.["application/json"] as Record<string, unknown> | undefined
1335
+ if (!json) return null
1336
+ let best: { code: number; schema: StandardSchemaLike } | null = null
1337
+ for (const [statusKey, schema] of Object.entries(json)) {
1338
+ if (schema === undefined) continue
1339
+ const code = statusKeyToCode[statusKey as keyof typeof statusKeyToCode]
1340
+ if (!code || code < 200 || code > 299) continue
1341
+ if (!best || code < best.code) best = { code, schema: schema as StandardSchemaLike }
1342
+ }
1343
+ return best?.schema ?? null
1344
+ }
1345
+
1346
+ /**
1347
+ * Reads app keys off the *root* of a route schema's JSON Schema. Vendor-neutral: Zod's
1348
+ * `.meta({ … })` lands on the JSON Schema root, and so does every other validator honey
1349
+ * supports that carries schema metadata.
1350
+ */
1351
+ /** Child schema nodes a deep search descends into, in visit order */
1352
+ function schemaChildren(node: Record<string, unknown>): Record<string, unknown>[] {
1353
+ const out: Record<string, unknown>[] = []
1354
+ const push = (value: unknown): void => {
1355
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
1356
+ out.push(value as Record<string, unknown>)
1357
+ }
1358
+ }
1359
+ const props = node.properties
1360
+ if (props !== null && typeof props === "object" && !Array.isArray(props)) {
1361
+ for (const child of Object.values(props as Record<string, unknown>)) push(child)
1362
+ }
1363
+ push(node.items)
1364
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
1365
+ const members = node[key]
1366
+ if (Array.isArray(members)) {
1367
+ for (const member of members) push(member)
1368
+ }
1369
+ }
1370
+ return out
1371
+ }
1372
+
1373
+ /** Bounded BFS — shallowest match wins; several distinct values at that depth is ambiguous */
1374
+ function searchSchemaKey(root: Record<string, unknown>, key: string, maxDepth: number): SchemaMetaHit {
1375
+ let level = [root]
1376
+ for (let depth = 0; depth <= maxDepth && level.length > 0; depth++) {
1377
+ const matches: unknown[] = []
1378
+ const seen = new Set<string>()
1379
+ for (const node of level) {
1380
+ if (!Object.hasOwn(node, key) || node[key] === undefined) continue
1381
+ const fingerprint = JSON.stringify(node[key]) ?? "undefined"
1382
+ if (seen.has(fingerprint)) continue
1383
+ seen.add(fingerprint)
1384
+ matches.push(node[key])
1385
+ }
1386
+ if (matches.length === 1) return { found: true, value: matches[0] }
1387
+ if (matches.length > 1) return { found: "ambiguous", values: matches }
1388
+ const next: Record<string, unknown>[] = []
1389
+ for (const node of level) next.push(...schemaChildren(node))
1390
+ level = next
1391
+ }
1392
+ return { found: false }
1393
+ }
1394
+
1395
+ const DEEP_SEARCH_MAX_DEPTH = 6
1396
+
1397
+ /**
1398
+ * Resolves one schema-metadata key against one schema source. `"root"` reads the schema root,
1399
+ * seeing through one level of `items` so a bare `array<Entity>` list output still finds the
1400
+ * descriptor stamped on the item. `"deep"` walks the schema for envelope shapes.
1401
+ */
1402
+ function makeSchemaMetaLookup(handler: RouteHandler): SchemaMetaLookup {
1403
+ const cache = new Map<string, Record<string, unknown> | undefined>()
1404
+
1405
+ const rootOf = (source: MetaSpecSchemaSource): Record<string, unknown> | undefined => {
1406
+ if (cache.has(source)) return cache.get(source)
1407
+ let schema: StandardSchemaLike | null = null
1408
+ let io: "input" | "output" = "output"
1409
+ if (source === "output") {
1410
+ schema = primaryOutputSchema(handler)
1411
+ } else {
1412
+ io = "input"
1413
+ const key = source.slice("input.".length) as keyof InputSchemasDef
1414
+ const entry = handler.iv?.[key]
1415
+ schema = entry ? unwrapEntry(entry as InputSchemaEntry) : null
1416
+ }
1417
+ let root: Record<string, unknown> | undefined
1418
+ if (schema) {
1419
+ const converted = asJsonSchema(schema, io)
1420
+ root = converted && typeof converted === "object" ? converted : undefined
1421
+ }
1422
+ cache.set(source, root)
1423
+ return root
1424
+ }
1425
+
1426
+ return (source, key, search) => {
1427
+ const root = rootOf(source)
1428
+ if (!root) return { found: false }
1429
+ if (search === "deep") return searchSchemaKey(root, key, DEEP_SEARCH_MAX_DEPTH)
1430
+ if (Object.hasOwn(root, key) && root[key] !== undefined) return { found: true, value: root[key] }
1431
+ /* a list output is `array<Entity>` — the descriptor sits on the item */
1432
+ if (root.type === "array") {
1433
+ const items = root.items
1434
+ if (items !== null && typeof items === "object" && !Array.isArray(items)) {
1435
+ const itemRoot = items as Record<string, unknown>
1436
+ if (Object.hasOwn(itemRoot, key) && itemRoot[key] !== undefined) {
1437
+ return { found: true, value: itemRoot[key] }
1438
+ }
1439
+ }
1440
+ }
1441
+ return { found: false }
1442
+ }
1443
+ }
1444
+
1353
1445
  export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1354
1446
  app: Honey<TEnv, TCtx, unknown, TMeta, unknown, string, string>,
1355
1447
  options: {
1356
1448
  filterRoutes?: (route: OpenApiRouteInfo<TMeta>) => boolean
1357
1449
  info: OpenApiInfo
1450
+ /** Named metaSpec profile — selects which emitted keys this document carries */
1451
+ profile?: string
1358
1452
  securitySchemes?: Record<string, unknown>
1359
1453
  },
1360
1454
  ): Promise<OpenApiSpec> {
1361
1455
  await Promise.all([loadToJSONSchema(), loadEffectJsonSchema()])
1456
+ const collector = new MetaSpecCollector()
1457
+ const metaSpec: CompiledMetaSpec = compileMetaSpec(getMetaSpecConfig(app), collector)
1458
+ const { filter: profileFilter, name: profileName } = resolveProfile(metaSpec, options.profile, collector)
1362
1459
  const factory = getErrorFactory(app)
1363
1460
  const errorMeta = getErrorMeta(factory)
1364
1461
 
@@ -1396,17 +1493,19 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1396
1493
  const responses: Record<string, unknown> = {}
1397
1494
  const parameters: Array<Record<string, unknown>> = []
1398
1495
 
1399
- /* meta → openApi operation fields (flat on meta) */
1400
- const meta = handler.mt as Record<string, unknown> | null
1401
- if (meta?.summary) operation.summary = meta.summary
1402
- if (meta?.description) operation.description = meta.description
1403
- if (meta?.tags) operation.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags
1404
- if (meta?.deprecated) operation.deprecated = meta.deprecated
1405
- if (meta?.operationId) operation.operationId = meta.operationId
1406
- if (meta?.security) operation.security = normalizeSecurity(meta.security)
1407
- const inv = meta?.invalidate
1408
- if (Array.isArray(inv) && inv.length > 0) operation["x-invalidate"] = inv
1409
- if (meta?.mcp === true) operation["x-mcp"] = true
1496
+ /* meta + schema meta → openApi operation fields, per the declared policy */
1497
+ applyMetaSpec({
1498
+ collector,
1499
+ filter: profileFilter,
1500
+ kind: "http",
1501
+ meta: handler.mt as Record<string, unknown> | null,
1502
+ method,
1503
+ operation,
1504
+ path,
1505
+ profile: profileName,
1506
+ schemaMeta: metaSpec.needsSchemas ? makeSchemaMetaLookup(handler) : undefined,
1507
+ spec: metaSpec,
1508
+ })
1410
1509
 
1411
1510
  /* path params */
1412
1511
  const params = extractParams(path)
@@ -1425,9 +1524,7 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1425
1524
  for (const [source, entry] of Object.entries(handler.iv)) {
1426
1525
  if (entry === undefined) continue
1427
1526
  const unwrapped =
1428
- "~standard" in (entry as object)
1429
- ? unwrapEntry(entry as InputSchemaEntry)
1430
- : (entry as Record<string, unknown>)
1527
+ "~standard" in (entry as object) ? unwrapEntry(entry as InputSchemaEntry) : (entry as Record<string, unknown>)
1431
1528
  const jsonSchema = asJsonSchema(unwrapped, "input")
1432
1529
 
1433
1530
  if (source === "json" || source === "form") {
@@ -1540,9 +1637,7 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1540
1637
  for (const [status, entries] of byStatus) {
1541
1638
  if (responses[String(status)] === undefined) {
1542
1639
  const standardKeys = entries.filter((e) => !e.schema).map((e) => e.key)
1543
- const customSchemas = entries
1544
- .filter((e) => e.schema)
1545
- .map((e) => e.schema as Record<string, unknown>)
1640
+ const customSchemas = entries.filter((e) => e.schema).map((e) => e.schema as Record<string, unknown>)
1546
1641
 
1547
1642
  let schema: Record<string, unknown>
1548
1643
 
@@ -1611,11 +1706,18 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1611
1706
  const operation: Record<string, unknown> = { "x-websocket": true }
1612
1707
  const parameters: Array<Record<string, unknown>> = []
1613
1708
 
1614
- const meta = handler.mt as Record<string, unknown> | null
1615
- if (meta?.summary) operation.summary = meta.summary
1616
- if (meta?.description) operation.description = meta.description
1617
- if (meta?.tags) operation.tags = typeof meta.tags === "string" ? [meta.tags] : meta.tags
1618
- if (meta?.operationId) operation.operationId = meta.operationId
1709
+ applyMetaSpec({
1710
+ collector,
1711
+ filter: profileFilter,
1712
+ kind: "ws",
1713
+ meta: handler.mt as Record<string, unknown> | null,
1714
+ method: "WS",
1715
+ operation,
1716
+ path,
1717
+ profile: profileName,
1718
+ schemaMeta: metaSpec.needsSchemas ? makeSchemaMetaLookup(handler as unknown as RouteHandler) : undefined,
1719
+ spec: metaSpec,
1720
+ })
1619
1721
 
1620
1722
  /* path params */
1621
1723
  const wsParams = extractParams(path)
@@ -1635,17 +1737,10 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1635
1737
  "_tag" in searchEntry
1636
1738
  ? (searchEntry as { schema: StandardSchemaLike }).schema
1637
1739
  : (searchEntry as StandardSchemaLike)
1638
- const jsonSchema = asJsonSchema(
1639
- searchSchema as StandardSchemaLike | Record<string, unknown>,
1640
- "input",
1641
- )
1740
+ const jsonSchema = asJsonSchema(searchSchema as StandardSchemaLike | Record<string, unknown>, "input")
1642
1741
  if (jsonSchema && typeof jsonSchema === "object") {
1643
- const props = (jsonSchema as Record<string, unknown>).properties as
1644
- | Record<string, unknown>
1645
- | undefined
1646
- const required = new Set(
1647
- ((jsonSchema as Record<string, unknown>).required ?? []) as string[],
1648
- )
1742
+ const props = (jsonSchema as Record<string, unknown>).properties as Record<string, unknown> | undefined
1743
+ const required = new Set(((jsonSchema as Record<string, unknown>).required ?? []) as string[])
1649
1744
  if (props) {
1650
1745
  for (const [name, schema] of Object.entries(props)) {
1651
1746
  parameters.push({
@@ -1665,6 +1760,8 @@ export async function generateOpenApi<TEnv, TCtx, TMeta = unknown>(
1665
1760
  paths[oaPath].get = operation
1666
1761
  }
1667
1762
 
1763
+ collector.flush()
1764
+
1668
1765
  const result: OpenApiSpec = {
1669
1766
  info: options.info,
1670
1767
  openapi: "3.1.0",
@@ -1738,9 +1835,7 @@ type RouteConfig = {
1738
1835
  path: string
1739
1836
  }
1740
1837
 
1741
- function serializeInputSchemas(
1742
- iv: InputSchemasDef | null,
1743
- ): Record<string, Record<string, unknown>> | null {
1838
+ function serializeInputSchemas(iv: InputSchemasDef | null): Record<string, Record<string, unknown>> | null {
1744
1839
  if (!iv) return null
1745
1840
  const result: Record<string, Record<string, unknown>> = {}
1746
1841
  for (const [source, entry] of Object.entries(iv)) {
@@ -1751,9 +1846,7 @@ function serializeInputSchemas(
1751
1846
  return Object.keys(result).length > 0 ? result : null
1752
1847
  }
1753
1848
 
1754
- function serializeOutputSchemas(
1755
- os: OutputSchemaDef | null,
1756
- ): Record<string, Record<string, unknown>> | null {
1849
+ function serializeOutputSchemas(os: OutputSchemaDef | null): Record<string, Record<string, unknown>> | null {
1757
1850
  if (!os) return null
1758
1851
  const result: Record<string, Record<string, unknown>> = {}
1759
1852
  for (const [contentType, schemas] of Object.entries(os)) {
@@ -1840,9 +1933,7 @@ function serializeNode(node: TreeBuild): string {
1840
1933
  : "null"
1841
1934
 
1842
1935
  const mExpr =
1843
- node.handlers.length > 0
1844
- ? `{ ${node.handlers.map((h) => `${h.method}: ${h.handler}`).join(", ")} }`
1845
- : "null"
1936
+ node.handlers.length > 0 ? `{ ${node.handlers.map((h) => `${h.method}: ${h.handler}`).join(", ")} }` : "null"
1846
1937
 
1847
1938
  return `{ s: ${sExpr}, d: ${dExpr}, w: ${wExpr}, m: ${mExpr}, ws: null }`
1848
1939
  }
@@ -1874,19 +1965,13 @@ export function generateRouteTree(routes: RouteConfig[]): string {
1874
1965
  /* emit handler configs with pre-built ek and mt */
1875
1966
  for (const { index, route } of handlerEntries) {
1876
1967
  const mwList = "[]"
1877
- const bekExpr =
1878
- route.boundaryErrorKey !== null ? JSON.stringify(route.boundaryErrorKey) : "null"
1879
- const ekSet =
1880
- route.errorKeys.length > 0 ? `new Set(${JSON.stringify(route.errorKeys)})` : "new Set()"
1968
+ const bekExpr = route.boundaryErrorKey !== null ? JSON.stringify(route.boundaryErrorKey) : "null"
1969
+ const ekSet = route.errorKeys.length > 0 ? `new Set(${JSON.stringify(route.errorKeys)})` : "new Set()"
1881
1970
  const mtExpr = route.meta !== null ? JSON.stringify(route.meta) : "null"
1882
1971
  const ivExpr =
1883
- route.inputSchemas !== null
1884
- ? `${JSON.stringify(route.inputSchemas)} as unknown as RouteHandler["iv"]`
1885
- : "null"
1972
+ route.inputSchemas !== null ? `${JSON.stringify(route.inputSchemas)} as unknown as RouteHandler["iv"]` : "null"
1886
1973
  const osExpr =
1887
- route.outputSchemas !== null
1888
- ? `${JSON.stringify(route.outputSchemas)} as unknown as RouteHandler["os"]`
1889
- : "null"
1974
+ route.outputSchemas !== null ? `${JSON.stringify(route.outputSchemas)} as unknown as RouteHandler["os"]` : "null"
1890
1975
  lines.push(
1891
1976
  `const H${index}: RouteHandler = { bek: ${bekExpr}, ef: null, fn: null as unknown as RouteHandler["fn"], mw: ${mwList}, ek: ${ekSet}, iv: ${ivExpr}, os: ${osExpr}, mt: ${mtExpr}, ov: null, rp: "" }`,
1892
1977
  )
@@ -1952,9 +2037,7 @@ export function generateRouteTree(routes: RouteConfig[]): string {
1952
2037
  const handlerMapEntries = handlerEntries.map(
1953
2038
  ({ index, route }) => `\t${JSON.stringify(`${route.method} ${route.path}`)}: H${index}`,
1954
2039
  )
1955
- lines.push(
1956
- `export const handlers: Record<string, RouteHandler> = {\n${handlerMapEntries.join(",\n")}\n}`,
1957
- )
2040
+ lines.push(`export const handlers: Record<string, RouteHandler> = {\n${handlerMapEntries.join(",\n")}\n}`)
1958
2041
  lines.push("")
1959
2042
 
1960
2043
  return lines.join("\n")
@@ -2020,8 +2103,7 @@ function generateFromTreeRoot(root: TreeNode): string {
2020
2103
  }
2021
2104
 
2022
2105
  if (metaEntries.length > 0) {
2023
- const metaType =
2024
- allKeys.size > 0 ? "Record<string, MetaShape>" : "Record<string, Record<string, unknown>>"
2106
+ const metaType = allKeys.size > 0 ? "Record<string, MetaShape>" : "Record<string, Record<string, unknown>>"
2025
2107
  code += `export const meta: ${metaType} = {\n${metaEntries.join(",\n")}\n}\n`
2026
2108
  }
2027
2109
 
@@ -2135,10 +2217,7 @@ function emitErrorType(handler: RouteHandler): string {
2135
2217
  .join(" | ")
2136
2218
  }
2137
2219
 
2138
- function emitErrorShapes(
2139
- handler: RouteHandler,
2140
- meta: Record<string, ErrorMetaEntry> | null,
2141
- ): string | null {
2220
+ function emitErrorShapes(handler: RouteHandler, meta: Record<string, ErrorMetaEntry> | null): string | null {
2142
2221
  if (handler.ek.size === 0) return null
2143
2222
  const entries: string[] = []
2144
2223
  for (const key of Array.from(handler.ek).sort()) {
@@ -2252,12 +2331,8 @@ export function generateTypes<TEnv, TCtx>(
2252
2331
  lines.push(`type TapMap = ${options.inlineTapsType}`)
2253
2332
  lines.push("")
2254
2333
  }
2255
- const ctxBase = options.inlineTapsType
2256
- ? `Omit<HoneyContext<${envType}>, "tap">`
2257
- : `HoneyContext<${envType}>`
2258
- const tapPart = options.inlineTapsType
2259
- ? " & { tap<K extends keyof TapMap>(key: K, payload: TapMap[K]): void }"
2260
- : ""
2334
+ const ctxBase = options.inlineTapsType ? `Omit<HoneyContext<${envType}>, "tap">` : `HoneyContext<${envType}>`
2335
+ const tapPart = options.inlineTapsType ? " & { tap<K extends keyof TapMap>(key: K, payload: TapMap[K]): void }" : ""
2261
2336
  lines.push(`export type ${baseCtxName} = ${ctxBase}${mwPart}${tapPart}`)
2262
2337
  lines.push("")
2263
2338
 
@@ -2372,8 +2447,7 @@ export function generateTypes<TEnv, TCtx>(
2372
2447
  }
2373
2448
  const errorsCtx = emitErrorsCtxType(handler)
2374
2449
  if (errorsCtx) additions.push(errorsCtx)
2375
- const ctxType =
2376
- additions.length === 0 ? basePart : `${basePart} & { ${additions.join("; ")} }`
2450
+ const ctxType = additions.length === 0 ? basePart : `${basePart} & { ${additions.join("; ")} }`
2377
2451
  const errorType = emitErrorType(handler)
2378
2452
  const metaType = emitMetaType(handler)
2379
2453
  const outputType = emitOutputType(handler)
@@ -2620,10 +2694,7 @@ export function jsonSchemaToTS(schema: Record<string, unknown> | undefined, dept
2620
2694
  }
2621
2695
 
2622
2696
  /* extract input type for an operation */
2623
- function emitSDKInputType(
2624
- op: Record<string, unknown>,
2625
- path: string,
2626
- ): { hasMandatory: boolean; type: string } {
2697
+ function emitSDKInputType(op: Record<string, unknown>, path: string): { hasMandatory: boolean; type: string } {
2627
2698
  const mandatoryParts: string[] = []
2628
2699
  const optionalParts: string[] = []
2629
2700
 
@@ -2657,7 +2728,7 @@ function emitSDKInputType(
2657
2728
  const schema = p.schema as Record<string, unknown> | undefined
2658
2729
  const tsType = jsonSchemaToTS(schema)
2659
2730
  const safeQueryName = sdkSafeName(name)
2660
- return `${safeQueryName}${required ? "" : "?"}: ${tsType}`
2731
+ return `${safeQueryName}${required ? "" : "?"}: ${tsType}`
2661
2732
  })
2662
2733
  const searchEntry = `search: { ${entries.join("; ")} }`
2663
2734
  if (hasRequired) {
@@ -2693,8 +2764,7 @@ function emitSDKInputType(
2693
2764
  optionalParts.push(entry)
2694
2765
  }
2695
2766
  }
2696
- const formContent =
2697
- content["multipart/form-data"] ?? content["application/x-www-form-urlencoded"]
2767
+ const formContent = content["multipart/form-data"] ?? content["application/x-www-form-urlencoded"]
2698
2768
  if (formContent?.schema) {
2699
2769
  const entry = `form: ${jsonSchemaToTS(formContent.schema as Record<string, unknown>)}`
2700
2770
  if (required) {
@@ -2738,9 +2808,7 @@ function emitSDKResponseType(op: Record<string, unknown>): string {
2738
2808
  continue
2739
2809
  }
2740
2810
  if (content["application/json"]?.schema) {
2741
- successTypes.push(
2742
- jsonSchemaToTS(content["application/json"].schema as Record<string, unknown>),
2743
- )
2811
+ successTypes.push(jsonSchemaToTS(content["application/json"].schema as Record<string, unknown>))
2744
2812
  } else if ("application/octet-stream" in content) {
2745
2813
  successTypes.push("ArrayBuffer")
2746
2814
  } else {
@@ -2760,9 +2828,7 @@ function emitSDKResponseType(op: Record<string, unknown>): string {
2760
2828
  }
2761
2829
 
2762
2830
  /* detect whether a JSON schema matches the standard error envelope shape */
2763
- export function isStandardErrEnvelope(
2764
- schema: Record<string, unknown>,
2765
- ): { keys: string[]; status: number } | null {
2831
+ export function isStandardErrEnvelope(schema: Record<string, unknown>): { keys: string[]; status: number } | null {
2766
2832
  const props = schema.properties as Record<string, Record<string, unknown>> | undefined
2767
2833
  if (!props) return null
2768
2834
 
@@ -2862,8 +2928,7 @@ function buildMethodSig(
2862
2928
  else if (m.sse) returnType = m.responseType
2863
2929
  else if (resolvedErr)
2864
2930
  returnType = `TThrow extends true ? Promise<${resolvedRes}> : Promise<SDKResult<${resolvedRes}, ${resolvedErr}>>`
2865
- else
2866
- returnType = `TThrow extends true ? Promise<${resolvedRes}> : Promise<SDKResult<${resolvedRes}>>`
2931
+ else returnType = `TThrow extends true ? Promise<${resolvedRes}> : Promise<SDKResult<${resolvedRes}>>`
2867
2932
  return { inputArg, returnType }
2868
2933
  }
2869
2934
 
@@ -2889,16 +2954,75 @@ function buildTypeAliases(
2889
2954
 
2890
2955
  /* TS reserved words that must be quoted when used as object/interface property keys */
2891
2956
  const SDK_TS_RESERVED = new Set([
2892
- "break", "case", "catch", "class", "const", "continue", "debugger",
2893
- "default", "delete", "do", "else", "enum", "export", "extends", "false",
2894
- "finally", "for", "function", "if", "import", "in", "instanceof", "new",
2895
- "null", "return", "super", "switch", "this", "throw", "true", "try",
2896
- "typeof", "var", "void", "while", "with", "yield", "let", "static",
2897
- "implements", "interface", "package", "private", "protected", "public",
2898
- "abstract", "as", "async", "await", "constructor", "declare", "from",
2899
- "get", "infer", "is", "keyof", "module", "namespace", "never", "of",
2900
- "readonly", "require", "set", "satisfies", "symbol", "type", "unique",
2901
- "unknown", "override",
2957
+ "break",
2958
+ "case",
2959
+ "catch",
2960
+ "class",
2961
+ "const",
2962
+ "continue",
2963
+ "debugger",
2964
+ "default",
2965
+ "delete",
2966
+ "do",
2967
+ "else",
2968
+ "enum",
2969
+ "export",
2970
+ "extends",
2971
+ "false",
2972
+ "finally",
2973
+ "for",
2974
+ "function",
2975
+ "if",
2976
+ "import",
2977
+ "in",
2978
+ "instanceof",
2979
+ "new",
2980
+ "null",
2981
+ "return",
2982
+ "super",
2983
+ "switch",
2984
+ "this",
2985
+ "throw",
2986
+ "true",
2987
+ "try",
2988
+ "typeof",
2989
+ "var",
2990
+ "void",
2991
+ "while",
2992
+ "with",
2993
+ "yield",
2994
+ "let",
2995
+ "static",
2996
+ "implements",
2997
+ "interface",
2998
+ "package",
2999
+ "private",
3000
+ "protected",
3001
+ "public",
3002
+ "abstract",
3003
+ "as",
3004
+ "async",
3005
+ "await",
3006
+ "constructor",
3007
+ "declare",
3008
+ "from",
3009
+ "get",
3010
+ "infer",
3011
+ "is",
3012
+ "keyof",
3013
+ "module",
3014
+ "namespace",
3015
+ "never",
3016
+ "of",
3017
+ "readonly",
3018
+ "require",
3019
+ "set",
3020
+ "satisfies",
3021
+ "symbol",
3022
+ "type",
3023
+ "unique",
3024
+ "unknown",
3025
+ "override",
2902
3026
  ])
2903
3027
 
2904
3028
  function sdkSafeName(name: string): string {
@@ -2947,16 +3071,12 @@ function buildSDKTypes(
2947
3071
  * single literal is long enough (>200 chars) to dominate the line.
2948
3072
  */
2949
3073
  const { aliases: resAliases, lines: resLines } = buildTypeAliases(
2950
- sdkMethods
2951
- .filter((m) => !m.ws && !m.sse && isHoistableResponseType(m.responseType))
2952
- .map((m) => m.responseType),
3074
+ sdkMethods.filter((m) => !m.ws && !m.sse && isHoistableResponseType(m.responseType)).map((m) => m.responseType),
2953
3075
  "_Res",
2954
3076
  2,
2955
3077
  )
2956
3078
  const { aliases: inpAliases, lines: inpLines } = buildTypeAliases(
2957
- sdkMethods
2958
- .filter((m) => m.inputType !== "{}" && isHoistableResponseType(m.inputType))
2959
- .map((m) => m.inputType),
3079
+ sdkMethods.filter((m) => m.inputType !== "{}" && isHoistableResponseType(m.inputType)).map((m) => m.inputType),
2960
3080
  "_Inp",
2961
3081
  2,
2962
3082
  )
@@ -2991,9 +3111,7 @@ function buildSDKTypes(
2991
3111
  /* TypedWebSocket */
2992
3112
  l.push("export type TypedWebSocket = {")
2993
3113
  l.push("\tclose(code?: number, reason?: string): void")
2994
- l.push(
2995
- '\toff(event: "close" | "error" | "message" | "open", handler: (...args: never[]) => void): void',
2996
- )
3114
+ l.push('\toff(event: "close" | "error" | "message" | "open", handler: (...args: never[]) => void): void')
2997
3115
  l.push('\ton(event: "close", handler: (code: number, reason: string) => void): void')
2998
3116
  l.push('\ton(event: "error", handler: (error: unknown) => void): void')
2999
3117
  l.push('\ton(event: "message", handler: (data: string) => void): void')
@@ -3012,9 +3130,7 @@ function buildSDKTypes(
3012
3130
  l.push(
3013
3131
  `\theaders?: Record<string, string> | ((ctx: { method: string; path: string }) => Record<string, string | undefined> | Promise<Record<string, string | undefined>>)`,
3014
3132
  )
3015
- l.push(
3016
- "\tinvalidation?: { maxSourcesPerTarget?: number; staleMaxEntries?: number; staleTime: number }",
3017
- )
3133
+ l.push("\tinvalidation?: { maxSourcesPerTarget?: number; staleMaxEntries?: number; staleTime: number }")
3018
3134
  l.push("\tmaxErrorMessageChars?: number")
3019
3135
  l.push("\tmode?: RequestMode")
3020
3136
  l.push(
@@ -3091,10 +3207,7 @@ function buildSDKTypes(
3091
3207
  function emitNestedMapNode(node: NestedServiceNode, indent: string): string[] {
3092
3208
  if (node.kind === "leaf") {
3093
3209
  const entry = node.entry
3094
- const parts = [
3095
- `method: ${JSON.stringify(entry.method)}`,
3096
- `path: ${JSON.stringify(entry.path)}`,
3097
- ]
3210
+ const parts = [`method: ${JSON.stringify(entry.method)}`, `path: ${JSON.stringify(entry.path)}`]
3098
3211
  if (entry.params) parts.push(`params: ${JSON.stringify(entry.params)}`)
3099
3212
  if (entry.sse) parts.push("sse: true")
3100
3213
  if (entry.ws) parts.push("ws: true")
@@ -3840,14 +3953,11 @@ type _RequestMeta = {
3840
3953
  }
3841
3954
 
3842
3955
  function sdkClientClientError(): string {
3843
- const subclassDecls = STATUS_ERROR_CLASSES
3844
- .map(({ name }) =>
3956
+ const subclassDecls = STATUS_ERROR_CLASSES.map(
3957
+ ({ name }) =>
3845
3958
  `class _${name} extends _ClientError { constructor(init: ConstructorParameters<typeof _ClientError>[0]) { super(init); this.name = "${name}" } }`,
3846
- )
3847
- .join("\n")
3848
- const mapEntries = STATUS_ERROR_CLASSES
3849
- .map(({ name, status }) => `\t${status}: _${name},`)
3850
- .join("\n")
3959
+ ).join("\n")
3960
+ const mapEntries = STATUS_ERROR_CLASSES.map(({ name, status }) => `\t${status}: _${name},`).join("\n")
3851
3961
  return `
3852
3962
  class _ClientError extends Error {
3853
3963
  \treadonly body: unknown
@@ -4891,7 +5001,7 @@ export function collectSDKMethods(spec: OpenApiSpecInput): {
4891
5001
 
4892
5002
  const segments = operationId.split(".")
4893
5003
  const isTopLevel = segments.length === 1
4894
- const resource = isTopLevel ? operationId : segments[0] ?? operationId
5004
+ const resource = isTopLevel ? operationId : (segments[0] ?? operationId)
4895
5005
  const action = isTopLevel ? "_call" : segments.slice(1).join(".")
4896
5006
 
4897
5007
  if (serviceMap[resource] === undefined) {
@@ -4934,11 +5044,7 @@ export function collectSDKMethods(spec: OpenApiSpecInput): {
4934
5044
  return { methods, nestedMap, serviceMap }
4935
5045
  }
4936
5046
 
4937
- function buildServiceEntryForOp(
4938
- op: Record<string, unknown>,
4939
- path: string,
4940
- method: string,
4941
- ): ServiceEntry {
5047
+ function buildServiceEntryForOp(op: Record<string, unknown>, path: string, method: string): ServiceEntry {
4942
5048
  const params = extractOpenApiPathParams(path)
4943
5049
  const entry: ServiceEntry = { method: method.toUpperCase(), path }
4944
5050
  if (params.length > 0) entry.params = params
@@ -4985,10 +5091,7 @@ function buildNestedServiceMap(
4985
5091
  return walkNs(ns)
4986
5092
  }
4987
5093
 
4988
- export function generateSDK(
4989
- spec: OpenApiSpecInput,
4990
- options?: { name?: string; stem?: string },
4991
- ): GeneratedSDK {
5094
+ export function generateSDK(spec: OpenApiSpecInput, options?: { name?: string; stem?: string }): GeneratedSDK {
4992
5095
  const sdkName = options?.name ?? "SDK"
4993
5096
  const stem = options?.stem ?? "sdk"
4994
5097
  const { methods: sdkMethods, nestedMap, serviceMap } = collectSDKMethods(spec)