@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
@@ -0,0 +1,8 @@
1
+ /** djb2 hash → 8-char lowercase hex. */
2
+ export function hashString(str: string): string {
3
+ let hash = 5381
4
+ for (let i = 0; i < str.length; i++) {
5
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0
6
+ }
7
+ return (hash >>> 0).toString(16).padStart(8, "0")
8
+ }
@@ -0,0 +1,517 @@
1
+ import { type OpenApiSpecInput, extractOpenApiPathParams, resolveRefs } from "./codegen.ts"
2
+
3
+ export type IRScalarType = "string" | "number" | "integer" | "boolean" | "null"
4
+
5
+ export type IRSchema =
6
+ | { kind: "scalar"; type: IRScalarType; format?: string; enum?: (string | number | boolean)[] }
7
+ | { kind: "const"; value: string | number | boolean }
8
+ | { kind: "object"; fields: IRField[]; additional?: IRSchema | false }
9
+ | { kind: "array"; items: IRSchema }
10
+ | { kind: "tuple"; items: IRSchema[] }
11
+ | { kind: "union"; variants: IRSchema[]; discriminator?: IRDiscriminator }
12
+ | { kind: "allOf"; parts: IRSchema[] }
13
+ | { kind: "ref"; name: string }
14
+ | { kind: "nullable"; inner: IRSchema }
15
+ | { kind: "binary" }
16
+ | { kind: "unknown" }
17
+
18
+ export type IRField = {
19
+ name: string /* raw JSON key, no mangling */
20
+ schema: IRSchema
21
+ required: boolean
22
+ description?: string
23
+ }
24
+
25
+ export type IRDiscriminator = {
26
+ propertyName: string
27
+ mapping?: Record<string, string> /* value → bare schema name */
28
+ }
29
+
30
+ export type IRParam = {
31
+ name: string
32
+ schema: IRSchema
33
+ description?: string
34
+ }
35
+
36
+ export type IRMultipartPart = {
37
+ name: string
38
+ /** "file" for schemas with kind === "binary" (or array-of-binary), otherwise "text". */
39
+ type: "file" | "text"
40
+ schema?: IRSchema
41
+ }
42
+
43
+ export type IRBody =
44
+ | { kind: "raw"; contentType: string; schema: IRSchema; required: boolean }
45
+ | { kind: "stream"; contentType: "application/octet-stream"; required: boolean }
46
+ | {
47
+ kind: "multipart"
48
+ contentType: "multipart/form-data"
49
+ parts: IRMultipartPart[]
50
+ required: boolean
51
+ }
52
+
53
+ export type IRResponse = {
54
+ status: string /* "200" | "204" | "default" | ... — preserved verbatim */
55
+ contentType?: string /* undefined for 204 / no-content */
56
+ schema?: IRSchema
57
+ }
58
+
59
+ export type IROperationExtensions = {
60
+ websocket?: true
61
+ realtime?: true
62
+ sse?: true
63
+ deprecated?: true
64
+ idempotencyKey?: true
65
+ mcp?: true
66
+ invalidates?: string[]
67
+ }
68
+
69
+ export type IROperation = {
70
+ id: string /* operationId */
71
+ method: string /* upper-cased HTTP method */
72
+ path: string /* OpenAPI path template, {param} preserved */
73
+ description?: string
74
+ params: {
75
+ path: IRParam[]
76
+ query: IRParam[]
77
+ header: IRParam[]
78
+ }
79
+ body?: IRBody
80
+ responses: Record<string, IRResponse>
81
+ extensions: IROperationExtensions
82
+ }
83
+
84
+ export type IRTreeEntry =
85
+ | { kind: "method"; op: IROperation }
86
+ | { kind: "namespace"; ns: IRNamespace }
87
+
88
+ export type IRNamespace = {
89
+ entries: Map<string, IRTreeEntry>
90
+ }
91
+
92
+ export type IR = {
93
+ operations: IROperation[]
94
+ schemas: Record<string, IRSchema>
95
+ tree: IRNamespace
96
+ }
97
+
98
+ export function methodsOf(ns: IRNamespace): Array<[string, IROperation]> {
99
+ return [...ns.entries.entries()]
100
+ .filter((e): e is [string, Extract<IRTreeEntry, { kind: "method" }>] => e[1].kind === "method")
101
+ .map(([k, v]): [string, IROperation] => [k, v.op])
102
+ .sort(([a], [b]) => a.localeCompare(b))
103
+ }
104
+
105
+ export function namespacesOf(ns: IRNamespace): Array<[string, IRNamespace]> {
106
+ return [...ns.entries.entries()]
107
+ .filter((e): e is [string, Extract<IRTreeEntry, { kind: "namespace" }>] => e[1].kind === "namespace")
108
+ .map(([k, v]): [string, IRNamespace] => [k, v.ns])
109
+ .sort(([a], [b]) => a.localeCompare(b))
110
+ }
111
+
112
+ function firstDescendantId(ns: IRNamespace): string {
113
+ for (const [, entry] of ns.entries) {
114
+ if (entry.kind === "method") return entry.op.id
115
+ const sub = firstDescendantId(entry.ns)
116
+ if (sub) return sub
117
+ }
118
+ return ""
119
+ }
120
+
121
+ export function buildResourceTree(operations: IROperation[]): IRNamespace {
122
+ const root: IRNamespace = { entries: new Map() }
123
+
124
+ for (const op of operations) {
125
+ const segments = op.id.split(".")
126
+ let cur = root
127
+
128
+ for (let i = 0; i < segments.length - 1; i++) {
129
+ const seg = segments[i] ?? ""
130
+ const existing = cur.entries.get(seg)
131
+ if (existing) {
132
+ if (existing.kind === "method") {
133
+ throw new Error(
134
+ `operationId conflict: "${existing.op.id}" is callable but also a namespace prefix of "${op.id}".\n` +
135
+ `Rename one. Suggested: "${existing.op.id}" → "${existing.op.id}One" or drop "${op.id}".`,
136
+ )
137
+ }
138
+ cur = existing.ns
139
+ } else {
140
+ const ns: IRNamespace = { entries: new Map() }
141
+ cur.entries.set(seg, { kind: "namespace", ns })
142
+ cur = ns
143
+ }
144
+ }
145
+
146
+ const leafName = segments[segments.length - 1] ?? ""
147
+ const existing = cur.entries.get(leafName)
148
+ if (existing && existing.kind === "namespace") {
149
+ const otherId = firstDescendantId(existing.ns)
150
+ throw new Error(
151
+ `operationId conflict: "${op.id}" is callable but also a namespace prefix of "${otherId}".\n` +
152
+ `Rename one. Suggested: "${op.id}" → "${op.id}One" or drop "${otherId}".`,
153
+ )
154
+ }
155
+ cur.entries.set(leafName, { kind: "method", op })
156
+ }
157
+
158
+ return root
159
+ }
160
+
161
+ function isNullableSchema(schema: Record<string, unknown>): boolean {
162
+ if (schema.nullable === true) return true
163
+ const t = schema.type
164
+ if (Array.isArray(t) && (t as unknown[]).includes("null")) return true
165
+ const variants = (schema.anyOf ?? schema.oneOf) as Record<string, unknown>[] | undefined
166
+ if (variants && variants.some((v) => v.type === "null")) return true
167
+ return false
168
+ }
169
+
170
+ function isStringEnum(schema: Record<string, unknown>): boolean {
171
+ const e = schema.enum as unknown[] | undefined
172
+ if (!e || e.length === 0) return false
173
+ const t = schema.type
174
+ if (t === "string") return true
175
+ if (!t && e.every((v) => typeof v === "string")) return true
176
+ return false
177
+ }
178
+
179
+ function isIntEnum(schema: Record<string, unknown>): boolean {
180
+ const e = schema.enum as unknown[] | undefined
181
+ if (!e || e.length === 0) return false
182
+ const t = schema.type
183
+ if (t === "integer") return true
184
+ if (!t && e.every((v) => typeof v === "number" && Number.isInteger(v))) return true
185
+ return false
186
+ }
187
+
188
+ /** Strip null variant from anyOf/oneOf; returns remaining schemas. */
189
+ function stripNullVariants(schema: Record<string, unknown>): Record<string, unknown>[] | null {
190
+ const variants = (schema.anyOf ?? schema.oneOf) as Record<string, unknown>[] | undefined
191
+ if (!variants) return null
192
+ return variants.filter((v) => v.type !== "null")
193
+ }
194
+
195
+ /** Strip "null" from a type array, returning the first non-null type string. */
196
+ function stripNullFromTypeArray(schema: Record<string, unknown>): Record<string, unknown> {
197
+ const t = schema.type as string[]
198
+ const nonNull = t.filter((v) => v !== "null")
199
+ return { ...schema, type: nonNull.length === 1 ? nonNull[0] : nonNull }
200
+ }
201
+
202
+ export function schemaToIR(schema: Record<string, unknown> | undefined): IRSchema {
203
+ if (!schema) return { kind: "unknown" }
204
+
205
+ if (typeof schema.$ref === "string") {
206
+ const parts = schema.$ref.split("/")
207
+ const name = parts[parts.length - 1] ?? schema.$ref
208
+ return { kind: "ref", name }
209
+ }
210
+
211
+ if (Array.isArray(schema.allOf)) {
212
+ const parts = (schema.allOf as Record<string, unknown>[]).map(schemaToIR)
213
+ return { kind: "allOf", parts }
214
+ }
215
+
216
+ /* const precedes type inference */
217
+ if (schema.const !== undefined) {
218
+ const v = schema.const
219
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
220
+ return { kind: "const", value: v }
221
+ }
222
+ }
223
+
224
+ if (isNullableSchema(schema)) {
225
+ let inner: IRSchema
226
+
227
+ if (Array.isArray(schema.type) && (schema.type as unknown[]).includes("null")) {
228
+ inner = schemaToIR(stripNullFromTypeArray(schema))
229
+ } else if (schema.anyOf || schema.oneOf) {
230
+ const remaining = stripNullVariants(schema) ?? []
231
+ if (remaining.length === 0) {
232
+ inner = { kind: "unknown" }
233
+ } else if (remaining.length === 1) {
234
+ inner = schemaToIR(remaining[0])
235
+ } else {
236
+ inner = { kind: "union", variants: remaining.map(schemaToIR) }
237
+ }
238
+ } else {
239
+ /* nullable: true on a plain schema — strip the flag and recurse */
240
+ const { nullable: _n, ...rest } = schema
241
+ inner = schemaToIR(rest as Record<string, unknown>)
242
+ }
243
+
244
+ /* idempotent: never double-wrap */
245
+ if (inner.kind === "nullable") return inner
246
+ return { inner, kind: "nullable" }
247
+ }
248
+
249
+ /* multi-type array without null — nullable path already handled null-containing arrays above */
250
+ if (Array.isArray(schema.type) && (schema.type as string[]).length > 1) {
251
+ const types = schema.type as string[]
252
+ const variants: IRSchema[] = types.map((t) => schemaToIR({ ...schema, type: t }))
253
+ if (variants.length === 1) return variants[0]
254
+ return { kind: "union", variants }
255
+ }
256
+
257
+ if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
258
+ const variants = ((schema.oneOf ?? schema.anyOf) as Record<string, unknown>[]).map(schemaToIR)
259
+ const rawDisc = schema.discriminator as Record<string, unknown> | undefined
260
+ if (rawDisc) {
261
+ const propertyName = rawDisc.propertyName as string
262
+ const rawMapping = rawDisc.mapping as Record<string, string> | undefined
263
+ let mapping: Record<string, string> | undefined
264
+ if (rawMapping) {
265
+ mapping = {}
266
+ for (const [k, v] of Object.entries(rawMapping)) {
267
+ const parts = v.split("/")
268
+ mapping[k] = parts[parts.length - 1]
269
+ }
270
+ }
271
+ const discriminator: IRDiscriminator = { propertyName }
272
+ if (mapping) discriminator.mapping = mapping
273
+ return { discriminator, kind: "union", variants }
274
+ }
275
+ return { kind: "union", variants }
276
+ }
277
+
278
+ /* enum detection precedes type branching — {enum:[...]} can lack type */
279
+ if (Array.isArray(schema.enum)) {
280
+ const enumVals = schema.enum as (string | number | boolean)[]
281
+ if (isStringEnum(schema)) return { enum: enumVals, kind: "scalar", type: "string" }
282
+ if (isIntEnum(schema)) return { enum: enumVals, kind: "scalar", type: "integer" }
283
+ /* mixed enum (string + number values) — emit as union of const literals */
284
+ if (enumVals.length > 0 && enumVals.every((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean")) {
285
+ return { kind: "union", variants: enumVals.map((v) => ({ kind: "const" as const, value: v })) }
286
+ }
287
+ }
288
+
289
+ if (schema.format === "binary" && (schema.type === "string" || schema.type === undefined)) {
290
+ return { kind: "binary" }
291
+ }
292
+
293
+ if (schema.type === "array" || Array.isArray(schema.items)) {
294
+ if (Array.isArray(schema.items)) {
295
+ const items = (schema.items as Record<string, unknown>[]).map(schemaToIR)
296
+ return { items, kind: "tuple" }
297
+ }
298
+ const items = schema.items ? schemaToIR(schema.items as Record<string, unknown>) : { kind: "unknown" as const }
299
+ return { items, kind: "array" }
300
+ }
301
+
302
+ if (
303
+ schema.type === "object" ||
304
+ schema.properties !== undefined ||
305
+ schema.additionalProperties !== undefined
306
+ ) {
307
+ const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>
308
+ const requiredKeys = new Set((schema.required as string[] | undefined) ?? [])
309
+ const fields: IRField[] = Object.entries(props).map(([name, propSchema]) => {
310
+ const field: IRField = {
311
+ name,
312
+ required: requiredKeys.has(name),
313
+ schema: schemaToIR(propSchema),
314
+ }
315
+ if (typeof propSchema.description === "string") field.description = propSchema.description
316
+ return field
317
+ })
318
+
319
+ let additional: IRSchema | false | undefined
320
+ const ap = schema.additionalProperties
321
+ if (ap === false) {
322
+ additional = false
323
+ } else if (ap === true) {
324
+ additional = { kind: "unknown" }
325
+ } else if (ap !== undefined) {
326
+ additional = schemaToIR(ap as Record<string, unknown>)
327
+ }
328
+
329
+ const result: { kind: "object"; fields: IRField[]; additional?: IRSchema | false } = {
330
+ fields,
331
+ kind: "object",
332
+ }
333
+ if (additional !== undefined) result.additional = additional
334
+ return result
335
+ }
336
+
337
+ const t = schema.type as string | undefined
338
+ if (t === "string" || t === "integer" || t === "number" || t === "boolean" || t === "null") {
339
+ const scalar: Extract<IRSchema, { kind: "scalar" }> = { kind: "scalar", type: t as IRScalarType }
340
+ if (typeof schema.format === "string") scalar.format = schema.format
341
+ if (Array.isArray(schema.enum)) scalar.enum = schema.enum as (string | number | boolean)[]
342
+ return scalar
343
+ }
344
+
345
+ return { kind: "unknown" }
346
+ }
347
+
348
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const
349
+
350
+ /** Detect whether a multipart/form-data schema has at least one binary (file) property. */
351
+ function hasBinaryMultipartPart(mediaType: Record<string, unknown> | undefined): boolean {
352
+ const schema = mediaType?.schema as Record<string, unknown> | undefined
353
+ const props = schema?.properties as Record<string, Record<string, unknown>> | undefined
354
+ if (!props) return false
355
+ for (const prop of Object.values(props)) {
356
+ if (prop.format === "binary") return true
357
+ if (prop.type === "array") {
358
+ const items = prop.items as Record<string, unknown> | undefined
359
+ if (items?.format === "binary") return true
360
+ }
361
+ }
362
+ return false
363
+ }
364
+
365
+ /** Normalise a multipart/form-data object schema into typed parts. */
366
+ function extractMultipartParts(
367
+ mediaType: Record<string, unknown> | undefined,
368
+ ): IRMultipartPart[] {
369
+ const schema = mediaType?.schema as Record<string, unknown> | undefined
370
+ const props = schema?.properties as Record<string, Record<string, unknown>> | undefined
371
+ if (!props) return []
372
+ const parts: IRMultipartPart[] = []
373
+ for (const [name, propSchema] of Object.entries(props)) {
374
+ const ir = schemaToIR(propSchema)
375
+ const isBinary =
376
+ ir.kind === "binary" || (ir.kind === "array" && ir.items.kind === "binary")
377
+ parts.push({ name, schema: ir, type: isBinary ? "file" : "text" })
378
+ }
379
+ return parts
380
+ }
381
+
382
+ function buildOperation(
383
+ id: string,
384
+ method: string,
385
+ path: string,
386
+ op: Record<string, unknown>,
387
+ ): IROperation {
388
+ const upperMethod = method.toUpperCase()
389
+
390
+ const declaredParams = (op.parameters as Record<string, unknown>[] | undefined) ?? []
391
+ const pathParams: IRParam[] = []
392
+ const queryParams: IRParam[] = []
393
+ const headerParams: IRParam[] = []
394
+ const declaredPathNames = new Set<string>()
395
+
396
+ for (const p of declaredParams) {
397
+ const pIn = p.in as string
398
+ const pName = p.name as string
399
+ const pSchema = p.schema ? schemaToIR(p.schema as Record<string, unknown>) : { kind: "scalar" as const, type: "string" as const }
400
+ const param: IRParam = { name: pName, schema: pSchema }
401
+ if (typeof p.description === "string") param.description = p.description
402
+ if (pIn === "path") {
403
+ pathParams.push(param)
404
+ declaredPathNames.add(pName)
405
+ } else if (pIn === "query") {
406
+ queryParams.push(param)
407
+ } else if (pIn === "header") {
408
+ headerParams.push(param)
409
+ }
410
+ /* cookie params discarded per OpenAPI client convention */
411
+ }
412
+
413
+ /* back-fill path params absent from parameters[] but present in the URL template */
414
+ for (const name of extractOpenApiPathParams(path)) {
415
+ if (!declaredPathNames.has(name)) {
416
+ pathParams.push({ name, schema: { kind: "scalar", type: "string" } })
417
+ }
418
+ }
419
+
420
+ let body: IRBody | undefined
421
+ const requestBody = op.requestBody as Record<string, unknown> | undefined
422
+ if (requestBody) {
423
+ const content = requestBody.content as Record<string, Record<string, unknown>> | undefined
424
+ const firstCt = content ? Object.keys(content)[0] : undefined
425
+ if (firstCt && content) {
426
+ const mediaType = content[firstCt]
427
+ const required = requestBody.required === true
428
+
429
+ if (firstCt === "application/octet-stream") {
430
+ body = { contentType: "application/octet-stream", kind: "stream", required }
431
+ } else if (firstCt === "multipart/form-data" && hasBinaryMultipartPart(mediaType)) {
432
+ body = {
433
+ contentType: "multipart/form-data",
434
+ kind: "multipart",
435
+ parts: extractMultipartParts(mediaType),
436
+ required,
437
+ }
438
+ } else {
439
+ const bodySchema = mediaType?.schema
440
+ ? schemaToIR(mediaType.schema as Record<string, unknown>)
441
+ : { kind: "unknown" as const }
442
+ body = { contentType: firstCt, kind: "raw", required, schema: bodySchema }
443
+ }
444
+ }
445
+ }
446
+
447
+ const responses: Record<string, IRResponse> = {}
448
+ const extensions: IROperationExtensions = {}
449
+
450
+ const rawResponses = op.responses as Record<string, Record<string, unknown>> | undefined
451
+ if (rawResponses) {
452
+ for (const [status, response] of Object.entries(rawResponses)) {
453
+ const content = response.content as Record<string, Record<string, unknown>> | undefined
454
+ if (!content || Object.keys(content).length === 0) {
455
+ responses[status] = { status }
456
+ } else {
457
+ const firstCt = Object.keys(content)[0]
458
+ /* SSE detected by response content-type, not a request extension */
459
+ if (firstCt === "text/event-stream") extensions.sse = true
460
+ const mediaType = content[firstCt]
461
+ const respSchema = mediaType?.schema
462
+ ? schemaToIR(mediaType.schema as Record<string, unknown>)
463
+ : undefined
464
+ const resp: IRResponse = { contentType: firstCt, status }
465
+ if (respSchema !== undefined) resp.schema = respSchema
466
+ responses[status] = resp
467
+ }
468
+ }
469
+ }
470
+
471
+ if (op["x-websocket"] === true) extensions.websocket = true
472
+ if (op["x-realtime"] === true) extensions.realtime = true
473
+ if (op["x-deprecated"] === true) extensions.deprecated = true
474
+ if (op["x-idempotency-key"] === true) extensions.idempotencyKey = true
475
+ if (op["x-mcp"] === true) extensions.mcp = true
476
+ if (Array.isArray(op["x-invalidate"])) extensions.invalidates = op["x-invalidate"] as string[]
477
+
478
+ const operation: IROperation = {
479
+ extensions,
480
+ id,
481
+ method: upperMethod,
482
+ params: { header: headerParams, path: pathParams, query: queryParams },
483
+ path,
484
+ responses,
485
+ }
486
+ if (body) operation.body = body
487
+ if (typeof op.description === "string") operation.description = op.description
488
+
489
+ return operation
490
+ }
491
+
492
+ export function toIR(spec: OpenApiSpecInput): IR {
493
+ /* schemas built from raw components BEFORE resolveRefs, which strips them */
494
+ const schemas: Record<string, IRSchema> = {}
495
+ for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
496
+ schemas[name] = schemaToIR(schema)
497
+ }
498
+
499
+ const resolved = resolveRefs(spec)
500
+ const operations: IROperation[] = []
501
+ const seen = new Set<string>()
502
+
503
+ for (const [path, pathItem] of Object.entries(resolved.paths)) {
504
+ for (const method of HTTP_METHODS) {
505
+ const op = pathItem[method] as Record<string, unknown> | undefined
506
+ if (!op) continue
507
+ const id = op.operationId as string | undefined
508
+ if (!id) continue
509
+ if (seen.has(id)) throw new Error(`Duplicate operationId: "${id}"`)
510
+ seen.add(id)
511
+ operations.push(buildOperation(id, method, path, op))
512
+ }
513
+ }
514
+
515
+ const tree = buildResourceTree(operations)
516
+ return { operations, schemas, tree }
517
+ }
@@ -0,0 +1,53 @@
1
+ /** Dynamic Zod/Effect JSON Schema loaders. Not a package export — tests may import this file. */
2
+
3
+ export let toJSONSchemaFn: ((schema: unknown, opts?: { io?: "input" | "output" }) => unknown) | undefined
4
+ let toJSONSchemaLoaded = false
5
+
6
+ export async function loadToJSONSchema(): Promise<
7
+ ((schema: unknown, opts?: { io?: "input" | "output" }) => unknown) | undefined
8
+ > {
9
+ if (toJSONSchemaLoaded) return toJSONSchemaFn
10
+ toJSONSchemaLoaded = true
11
+ try {
12
+ const id = "zod"
13
+ const zod = await import(id)
14
+ if (typeof zod.toJSONSchema === "function") {
15
+ toJSONSchemaFn = zod.toJSONSchema as (
16
+ schema: unknown,
17
+ opts?: { io?: "input" | "output" },
18
+ ) => unknown
19
+ }
20
+ } catch {
21
+ /* zod not available */
22
+ }
23
+ return toJSONSchemaFn
24
+ }
25
+
26
+ export let effectJsonSchemaFn: ((schema: unknown) => unknown) | undefined
27
+ let effectLoaded = false
28
+
29
+ export async function loadEffectJsonSchema(): Promise<((schema: unknown) => unknown) | undefined> {
30
+ if (effectLoaded) return effectJsonSchemaFn
31
+ effectLoaded = true
32
+ try {
33
+ const id = "effect"
34
+ const effect = await import(id)
35
+ if (effect.JSONSchema && typeof effect.JSONSchema.make === "function") {
36
+ effectJsonSchemaFn = effect.JSONSchema.make as (schema: unknown) => unknown
37
+ }
38
+ } catch {
39
+ /* effect not available */
40
+ }
41
+ return effectJsonSchemaFn
42
+ }
43
+
44
+ export async function prepareCodegen(): Promise<void> {
45
+ await Promise.all([loadToJSONSchema(), loadEffectJsonSchema()])
46
+ }
47
+
48
+ export function resetCodegenJsonSchemaLoadersForTests(): void {
49
+ toJSONSchemaFn = undefined
50
+ toJSONSchemaLoaded = true
51
+ effectJsonSchemaFn = undefined
52
+ effectLoaded = true
53
+ }