@workos/oagen 0.3.0 → 0.4.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.
package/dist/smoke.mjs CHANGED
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
- import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
6
+ import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
7
7
  //#region src/utils/naming.ts
8
8
  /**
9
9
  * Known compound tokens that the regex-based splitter over-splits.
@@ -86,11 +86,7 @@ function toSnakeCase(s) {
86
86
  function toUpperSnakeCase(s) {
87
87
  return splitWords(s).map((w) => w.toUpperCase()).join("_");
88
88
  }
89
- const BACKEND_SUFFIXES = [
90
- "Dto",
91
- "DTO",
92
- "Controller"
93
- ];
89
+ const BACKEND_SUFFIXES = ["Controller"];
94
90
  function stripBackendSuffixes(name) {
95
91
  for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
96
92
  return name;
@@ -117,7 +113,9 @@ const SINGULAR_SAFE_LIST = new Set([
117
113
  "Canvas",
118
114
  "Alias",
119
115
  "Basis",
120
- "Bus"
116
+ "Bus",
117
+ "Species",
118
+ "Series"
121
119
  ]);
122
120
  /**
123
121
  * Conservative singularize for the leading PascalCase word.
@@ -145,6 +143,120 @@ function cleanSchemaName(name) {
145
143
  return result;
146
144
  }
147
145
  //#endregion
146
+ //#region src/ir/sdk-behavior.ts
147
+ /**
148
+ * Canonical SDK behavior defaults. These match the PHP emitter's current
149
+ * implementation (the most complete). Per-language overrides can be applied
150
+ * via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
151
+ *
152
+ * - Retry: exponential backoff with jitter, retries on 429/5xx
153
+ * - Errors: standard HTTP status → exception kind mapping
154
+ * - Telemetry: enabled by default, tracks request ID + latency
155
+ * - Pagination: no inter-page delay (Node overrides to 350ms)
156
+ * - Idempotency: auto-generate UUID v4 for retryable POSTs
157
+ * - Logging: all lifecycle events enabled
158
+ * - User-Agent: SDK identifier + runtime version + app info + AI agent detection
159
+ * - Request guards: detect misplaced RequestOptions keys in params
160
+ * - Timeout: 60s default (Python overrides to 30s)
161
+ */
162
+ function defaultSdkBehavior() {
163
+ return {
164
+ retry: {
165
+ retryableStatusCodes: [
166
+ 429,
167
+ 500,
168
+ 502,
169
+ 503,
170
+ 504
171
+ ],
172
+ maxRetries: 3,
173
+ retryOnConnectionError: true,
174
+ retryOnTimeout: true,
175
+ backoff: {
176
+ initialDelay: 1,
177
+ multiplier: 2,
178
+ maxDelay: 30,
179
+ jitterFactor: .5
180
+ }
181
+ },
182
+ errors: {
183
+ statusCodeMap: {
184
+ 400: "BadRequest",
185
+ 401: "Authentication",
186
+ 403: "Authorization",
187
+ 404: "NotFound",
188
+ 409: "Conflict",
189
+ 422: "UnprocessableEntity",
190
+ 429: "RateLimitExceeded"
191
+ },
192
+ serverErrorKind: "Server",
193
+ clientErrorKind: "Api",
194
+ errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
195
+ },
196
+ telemetry: {
197
+ enabledByDefault: true,
198
+ headerName: "X-WorkOS-Client-Telemetry",
199
+ requestIdHeader: "X-Request-ID"
200
+ },
201
+ pagination: { autoPageDelayMs: 0 },
202
+ idempotency: {
203
+ headerName: "Idempotency-Key",
204
+ autoGenerateForPost: true
205
+ },
206
+ logging: {
207
+ enabled: true,
208
+ events: [
209
+ "request.start",
210
+ "request.success",
211
+ "request.retry",
212
+ "request.rate_limited",
213
+ "request.error",
214
+ "request.connection_error"
215
+ ]
216
+ },
217
+ userAgent: {
218
+ sdkIdentifierTemplate: "{name} {lang}/{version}",
219
+ includeRuntimeVersion: true,
220
+ allowAppInfo: true,
221
+ aiAgentEnvVars: [
222
+ {
223
+ envVar: "CLAUDE_CODE",
224
+ agentName: "ClaudeCode"
225
+ },
226
+ {
227
+ envVar: "CURSOR_AGENT",
228
+ agentName: "Cursor"
229
+ },
230
+ {
231
+ envVar: "CLINE_ACTIVE",
232
+ agentName: "Cline"
233
+ },
234
+ {
235
+ envVar: "WINDSURF_ACTIVE",
236
+ agentName: "Windsurf"
237
+ },
238
+ {
239
+ envVar: "COPILOT_AGENT",
240
+ agentName: "Copilot"
241
+ }
242
+ ]
243
+ },
244
+ requestGuard: { optionKeys: [
245
+ "api_key",
246
+ "apiKey",
247
+ "idempotency_key",
248
+ "idempotencyKey",
249
+ "extra_headers",
250
+ "extraHeaders",
251
+ "max_retries",
252
+ "maxRetries",
253
+ "base_url",
254
+ "baseUrl"
255
+ ] },
256
+ timeout: { defaultTimeoutSeconds: 60 }
257
+ };
258
+ }
259
+ //#endregion
148
260
  //#region src/errors.ts
149
261
  /**
150
262
  * Structured error hierarchy for oagen.
@@ -173,13 +285,11 @@ var SpecParseError = class extends OagenError {
173
285
  //#region src/parser/refs.ts
174
286
  async function loadAndBundleSpec(specPath) {
175
287
  const absolutePath = resolve(specPath);
176
- const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
177
- const config = await createConfig({});
178
- const resolver = new BaseResolver();
179
288
  const result = await bundleDocument({
180
- document,
181
- config: config.styleguide,
182
- externalRefResolver: resolver,
289
+ document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
290
+ config: await createConfig({}),
291
+ types: Oas3_1Types,
292
+ externalRefResolver: new BaseResolver(),
183
293
  dereference: false
184
294
  });
185
295
  if (result.problems.some((p) => p.severity === "error")) throw new SpecParseError(`Failed to parse spec: ${result.problems.filter((p) => p.severity === "error").map((p) => p.message).join("\n")}`, `Check the OpenAPI spec at "${absolutePath}" for syntax errors. Run a linter such as \`npx @redocly/cli lint ${specPath}\` to identify issues.`);
@@ -321,15 +431,39 @@ function buildInlineModel(name, schema) {
321
431
  }
322
432
  //#endregion
323
433
  //#region src/parser/schemas.ts
324
- function extractSchemas(schemas) {
434
+ /**
435
+ * Module-level transform set during extractSchemas(). Used by schemaToTypeRef()
436
+ * to apply the same name transform to $ref model/enum references.
437
+ */
438
+ let activeSchemaNameTransform = null;
439
+ /** Apply cleanSchemaName + the active transform (if any) to a raw schema name. */
440
+ function resolveSchemaName(rawName) {
441
+ let name = cleanSchemaName(toPascalCase(rawName));
442
+ if (activeSchemaNameTransform) name = activeSchemaNameTransform(name);
443
+ return name;
444
+ }
445
+ function extractSchemas(schemas, options) {
325
446
  const enums = [];
326
447
  if (!schemas) return {
327
448
  models: [],
328
449
  enums
329
450
  };
451
+ if (options?.schemaNameTransform) {
452
+ const transform = options.schemaNameTransform;
453
+ const cleanedNames = Object.keys(schemas).map((n) => cleanSchemaName(toPascalCase(n)));
454
+ const transformedToOriginals = /* @__PURE__ */ new Map();
455
+ for (const cleaned of cleanedNames) {
456
+ const transformed = transform(cleaned);
457
+ if (!transformedToOriginals.has(transformed)) transformedToOriginals.set(transformed, []);
458
+ transformedToOriginals.get(transformed).push(cleaned);
459
+ }
460
+ const unsafeToTransform = /* @__PURE__ */ new Set();
461
+ for (const [, originals] of transformedToOriginals) if (originals.length > 1) for (const n of originals) unsafeToTransform.add(n);
462
+ activeSchemaNameTransform = (name) => unsafeToTransform.has(name) ? name : transform(name);
463
+ } else activeSchemaNameTransform = null;
330
464
  const modelsByCleanName = /* @__PURE__ */ new Map();
331
465
  for (const [name, schema] of Object.entries(schemas)) {
332
- const pascalName = cleanSchemaName(toPascalCase(name));
466
+ const pascalName = resolveSchemaName(name);
333
467
  if (schema.enum) enums.push(extractEnum(pascalName, schema));
334
468
  else {
335
469
  const model = extractModel(pascalName, schema, schemas);
@@ -337,6 +471,12 @@ function extractSchemas(schemas) {
337
471
  if (existing) {
338
472
  if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
339
473
  } else modelsByCleanName.set(pascalName, model);
474
+ for (const inlineModel of extractDiscriminatedAllOfVariantModels(schema, pascalName)) {
475
+ const existingInline = modelsByCleanName.get(inlineModel.name);
476
+ if (existingInline) {
477
+ if (inlineModel.fields.length > existingInline.fields.length) modelsByCleanName.set(inlineModel.name, inlineModel);
478
+ } else modelsByCleanName.set(inlineModel.name, inlineModel);
479
+ }
340
480
  }
341
481
  }
342
482
  const models = [...modelsByCleanName.values()];
@@ -352,6 +492,10 @@ function extractSchemas(schemas) {
352
492
  enums
353
493
  };
354
494
  }
495
+ /** Clear the module-level schemaNameTransform. Called by parseSpec() after all extraction phases. */
496
+ function clearSchemaNameTransform() {
497
+ activeSchemaNameTransform = null;
498
+ }
355
499
  /**
356
500
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
357
501
  * Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
@@ -388,7 +532,7 @@ function collectInlineEnums(fields, enums) {
388
532
  function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
389
533
  walkTypeRef(ref, { model: (modelRef) => {
390
534
  if (modelNames.has(modelRef.name)) return;
391
- if (Object.keys(schemas).some((k) => cleanSchemaName(toPascalCase(k)) === modelRef.name)) return;
535
+ if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
392
536
  const parentSchema = findSchemaByName(parentModelName, schemas);
393
537
  if (!parentSchema) return;
394
538
  const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
@@ -403,7 +547,7 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
403
547
  }
404
548
  /** Find a component schema by PascalCase name. */
405
549
  function findSchemaByName(pascalName, schemas) {
406
- for (const [k, v] of Object.entries(schemas)) if (cleanSchemaName(toPascalCase(k)) === pascalName) return v;
550
+ for (const [k, v] of Object.entries(schemas)) if (resolveSchemaName(k) === pascalName) return v;
407
551
  return null;
408
552
  }
409
553
  /** Walk into a schema to find a field's sub-schema, resolving allOf. */
@@ -561,13 +705,88 @@ function extractAllOfModel(name, schema, schemas) {
561
705
  fields
562
706
  };
563
707
  }
708
+ function extractDiscriminatedAllOfVariantModels(schema, fallbackName) {
709
+ const models = [];
710
+ const seenNames = /* @__PURE__ */ new Set();
711
+ for (const subSchema of schema.allOf ?? []) for (const variant of subSchema.oneOf ?? []) {
712
+ if (!variant.properties || variant.type !== void 0 && variant.type !== "object") continue;
713
+ const variantName = deriveDiscriminatedVariantName(variant, fallbackName);
714
+ for (const model of extractInlineModelDeep(variantName, variant)) {
715
+ if (seenNames.has(model.name)) continue;
716
+ seenNames.add(model.name);
717
+ models.push(model);
718
+ }
719
+ }
720
+ return models;
721
+ }
722
+ function deriveDiscriminatedVariantName(schema, fallbackName) {
723
+ if (!schema.properties) return fallbackName;
724
+ for (const name of [
725
+ "event",
726
+ "type",
727
+ "object"
728
+ ]) {
729
+ const field = schema.properties[name];
730
+ if (!field) continue;
731
+ if (typeof field.const === "string") return toPascalCase(field.const);
732
+ if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
733
+ }
734
+ for (const field of Object.values(schema.properties)) {
735
+ if (!field) continue;
736
+ if (typeof field.const === "string") return toPascalCase(field.const);
737
+ if (field.enum && field.enum.length === 1 && typeof field.enum[0] === "string") return toPascalCase(field.enum[0]);
738
+ }
739
+ return fallbackName;
740
+ }
741
+ function extractInlineModelDeep(name, schema) {
742
+ const requiredSet = new Set(schema.required ?? []);
743
+ const fields = [];
744
+ const properties = schema.properties ?? {};
745
+ const nestedModels = [];
746
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
747
+ if (!fieldSchema) continue;
748
+ fields.push(buildFieldFromSchema(fieldName, fieldSchema, name, requiredSet));
749
+ if (fieldSchema.type === "object" && fieldSchema.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema));
750
+ if (fieldSchema.type === "array" && fieldSchema.items?.properties) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), fieldSchema.items));
751
+ if (fieldSchema.allOf) {
752
+ const mergedProperties = {};
753
+ const mergedRequired = [];
754
+ for (const sub of fieldSchema.allOf) {
755
+ if (sub.properties) Object.assign(mergedProperties, sub.properties);
756
+ if (sub.required) mergedRequired.push(...sub.required);
757
+ }
758
+ if (Object.keys(mergedProperties).length > 0) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), {
759
+ type: "object",
760
+ properties: mergedProperties,
761
+ required: mergedRequired
762
+ }));
763
+ }
764
+ if (fieldSchema.oneOf) {
765
+ const objectVariant = fieldSchema.oneOf.find((v) => v.properties && (v.type === "object" || !v.type));
766
+ if (objectVariant) nestedModels.push(...extractInlineModelDeep(qualifyNestedInlineName(name, fieldName), objectVariant));
767
+ }
768
+ }
769
+ return [{
770
+ name,
771
+ description: schema.description,
772
+ fields
773
+ }, ...nestedModels];
774
+ }
775
+ function qualifyNestedInlineName(parentName, fieldName) {
776
+ const pascalField = toPascalCase(fieldName);
777
+ if (pascalField.startsWith(parentName)) return pascalField;
778
+ const qualified = `${stripListItemMarkers(parentName)}${pascalField}`;
779
+ const match = qualified.match(/^(.+?)([A-Z][a-z]+s?)$/);
780
+ if (match && match[2]) return match[1] + singularize(match[2]);
781
+ return qualified;
782
+ }
564
783
  function schemaToTypeRef(schema, contextName, parentModelName) {
565
784
  if (schema.$ref) {
566
785
  const segments = schema.$ref.split("/");
567
786
  const rawName = segments[segments.length - 1];
568
787
  return {
569
788
  kind: "model",
570
- name: cleanSchemaName(toPascalCase(rawName))
789
+ name: resolveSchemaName(rawName)
571
790
  };
572
791
  }
573
792
  if (Array.isArray(schema.type)) {
@@ -831,6 +1050,7 @@ const KNOWN_DATA_PATHS = new Set([
831
1050
  "edges"
832
1051
  ]);
833
1052
  const PAGINATION_METADATA_PATTERNS = [
1053
+ "meta",
834
1054
  "metadata",
835
1055
  "pagination",
836
1056
  "cursor",
@@ -862,11 +1082,12 @@ function detectListEnvelope(schema) {
862
1082
  }
863
1083
  if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
864
1084
  const arrayPropName = arrayProps[0];
865
- if (KNOWN_DATA_PATHS.has(arrayPropName)) return {
1085
+ const hasPaginationCompanion = nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)));
1086
+ if (KNOWN_DATA_PATHS.has(arrayPropName) && hasPaginationCompanion) return {
866
1087
  isEnvelope: true,
867
1088
  dataPath: arrayPropName
868
1089
  };
869
- if (nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)))) return {
1090
+ if (hasPaginationCompanion) return {
870
1091
  isEnvelope: true,
871
1092
  dataPath: arrayPropName
872
1093
  };
@@ -1170,13 +1391,33 @@ function extractOperations(paths, operationIdTransform) {
1170
1391
  const op = pathItem[method];
1171
1392
  if (!op) continue;
1172
1393
  const serviceName = inferServiceName(path, op.tags?.[0]);
1173
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
1394
+ const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName);
1174
1395
  inlineModels.push(...opModels);
1175
1396
  const ops = serviceMap.get(serviceName) ?? [];
1176
1397
  ops.push(operation);
1177
1398
  serviceMap.set(serviceName, ops);
1178
1399
  }
1179
1400
  }
1401
+ for (const [serviceName, operations] of serviceMap) {
1402
+ const pathGroups = /* @__PURE__ */ new Map();
1403
+ for (const op of operations) {
1404
+ const firstSeg = op.path.split("/").filter(Boolean)[0] ?? "";
1405
+ const groupKey = firstSeg.startsWith("{") ? serviceName : toPascalCase(firstSeg);
1406
+ const group = pathGroups.get(groupKey) ?? [];
1407
+ group.push(op);
1408
+ pathGroups.set(groupKey, group);
1409
+ }
1410
+ if (pathGroups.size > 1) {
1411
+ if ([...pathGroups.keys()].some((k) => k !== serviceName)) {
1412
+ serviceMap.delete(serviceName);
1413
+ for (const [groupName, groupOps] of pathGroups) {
1414
+ const existing = serviceMap.get(groupName) ?? [];
1415
+ existing.push(...groupOps);
1416
+ serviceMap.set(groupName, existing);
1417
+ }
1418
+ }
1419
+ }
1420
+ }
1180
1421
  for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
1181
1422
  return {
1182
1423
  services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
@@ -1292,13 +1533,14 @@ function inferDeeperContext(path) {
1292
1533
  if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
1293
1534
  return null;
1294
1535
  }
1295
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
1536
+ function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
1296
1537
  const allParams = [...pathLevelParams, ...op.parameters ?? []];
1297
1538
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
1298
- const pathParams = extractParams(allParams, "path");
1299
- const queryParams = extractParams(allParams, "query");
1300
- const headerParams = extractParams(allParams, "header").filter((p) => p.name.toLowerCase() !== "idempotency-key");
1301
- const cookieParams = extractParams(allParams, "cookie");
1539
+ const opContext = serviceName;
1540
+ const pathParams = extractParams(allParams, "path", opContext);
1541
+ const queryParams = extractParams(allParams, "query", opContext);
1542
+ const headerParams = extractParams(allParams, "header", opContext).filter((p) => p.name.toLowerCase() !== "idempotency-key");
1543
+ const cookieParams = extractParams(allParams, "cookie", opContext);
1302
1544
  const reqBodyModels = [];
1303
1545
  const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
1304
1546
  const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
@@ -1315,6 +1557,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
1315
1557
  itemType
1316
1558
  };
1317
1559
  } else if (paginationFromParams) pagination = paginationFromParams;
1560
+ const security = extractOperationSecurity(op.security);
1318
1561
  return {
1319
1562
  operation: {
1320
1563
  name: inferOperationName(method, path, op.operationId, operationIdTransform),
@@ -1333,19 +1576,35 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
1333
1576
  pagination,
1334
1577
  injectIdempotencyKey: hasIdempotencyHeader,
1335
1578
  deprecated: op.deprecated || void 0,
1336
- async: op["x-oagen-async"]
1579
+ async: op["x-oagen-async"],
1580
+ security
1337
1581
  },
1338
1582
  inlineModels
1339
1583
  };
1340
1584
  }
1585
+ /**
1586
+ * Extract per-operation security requirements from an OpenAPI security directive.
1587
+ * Returns undefined when the operation uses the spec-level default security.
1588
+ *
1589
+ * OpenAPI security format: `security: [{ schemeName: [scope1, scope2] }]`
1590
+ */
1591
+ function extractOperationSecurity(security) {
1592
+ if (!security || security.length === 0) return void 0;
1593
+ const requirements = [];
1594
+ for (const entry of security) for (const [schemeName, scopes] of Object.entries(entry)) requirements.push({
1595
+ schemeName,
1596
+ scopes: scopes ?? []
1597
+ });
1598
+ return requirements.length > 0 ? requirements : void 0;
1599
+ }
1341
1600
  function buildDescription(summary, description) {
1342
1601
  if (summary && description && summary !== description) return `${summary}\n\n${description}`;
1343
1602
  return description ?? summary;
1344
1603
  }
1345
- function extractParams(params, location) {
1604
+ function extractParams(params, location, operationContext) {
1346
1605
  return params.filter((p) => p.in === location).map((p) => ({
1347
1606
  name: p.name,
1348
- type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
1607
+ type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
1349
1608
  kind: "primitive",
1350
1609
  type: "string"
1351
1610
  },
@@ -1531,7 +1790,14 @@ function extractResponses(responses, op, path, method) {
1531
1790
  statusCode: code,
1532
1791
  type: extractedType
1533
1792
  });
1534
- } else if (code >= 400) {
1793
+ } else if (code >= 300 && code < 400) allSuccessResponses.push({
1794
+ statusCode: code,
1795
+ type: {
1796
+ kind: "primitive",
1797
+ type: "unknown"
1798
+ }
1799
+ });
1800
+ else if (code >= 400) {
1535
1801
  const jsonContent = resp.content?.["application/json"];
1536
1802
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
1537
1803
  errors.push({
@@ -1658,6 +1924,19 @@ function collectInlineEnumsFromModels(models, enums) {
1658
1924
  const enumNames = new Set(enums.map((e) => e.name));
1659
1925
  for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
1660
1926
  }
1927
+ /**
1928
+ * Collect inline enum definitions from operation parameters (query, path, header)
1929
+ * and promote them to top-level enums so emitters can generate typed enum files.
1930
+ */
1931
+ function collectInlineEnumsFromOperations(services, enums) {
1932
+ const enumNames = new Set(enums.map((e) => e.name));
1933
+ for (const service of services) for (const op of service.operations) for (const param of [
1934
+ ...op.pathParams,
1935
+ ...op.queryParams,
1936
+ ...op.headerParams,
1937
+ ...op.cookieParams ?? []
1938
+ ]) collectInlineEnumFromRef(param.type, enums, enumNames);
1939
+ }
1661
1940
  //#endregion
1662
1941
  //#region src/parser/normalize-model-refs.ts
1663
1942
  function validateModelRefs(spec) {
@@ -1687,10 +1966,12 @@ async function parseSpec(specPath, options) {
1687
1966
  const spec = parsed;
1688
1967
  const version = spec.openapi ?? "";
1689
1968
  if (!version.startsWith("3.")) throw new SpecParseError(`Unsupported OpenAPI version: ${version}. oagen requires OpenAPI 3.x`, `Update the spec to OpenAPI 3.0 or 3.1. If you are using Swagger 2.x, convert it first with \`npx swagger2openapi ${specPath}\`.`);
1690
- const { models, enums } = extractSchemas(spec.components?.schemas);
1969
+ const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
1691
1970
  const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
1971
+ clearSchemaNameTransform();
1692
1972
  const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
1693
1973
  collectInlineEnumsFromModels(finalModels, enums);
1974
+ collectInlineEnumsFromOperations(services, enums);
1694
1975
  const auth = extractAuthSchemes(spec.components?.securitySchemes);
1695
1976
  const serverEntries = (spec.servers ?? []).map((s) => ({
1696
1977
  url: s.url ?? "",
@@ -1705,7 +1986,8 @@ async function parseSpec(specPath, options) {
1705
1986
  services,
1706
1987
  models: finalModels,
1707
1988
  enums,
1708
- auth
1989
+ auth,
1990
+ sdk: defaultSdkBehavior()
1709
1991
  };
1710
1992
  validateModelRefs(result);
1711
1993
  return result;