@workos/oagen 0.2.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.
@@ -2,8 +2,122 @@ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
- import { BaseResolver, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
5
+ import { BaseResolver, Oas3_1Types, bundleDocument, createConfig, makeDocumentFromString } from "@redocly/openapi-core";
6
6
  import "dotenv/config";
7
+ //#region src/ir/sdk-behavior.ts
8
+ /**
9
+ * Canonical SDK behavior defaults. These match the PHP emitter's current
10
+ * implementation (the most complete). Per-language overrides can be applied
11
+ * via `mergeSdkBehavior()` in each SDK's `oagen.config.ts`.
12
+ *
13
+ * - Retry: exponential backoff with jitter, retries on 429/5xx
14
+ * - Errors: standard HTTP status → exception kind mapping
15
+ * - Telemetry: enabled by default, tracks request ID + latency
16
+ * - Pagination: no inter-page delay (Node overrides to 350ms)
17
+ * - Idempotency: auto-generate UUID v4 for retryable POSTs
18
+ * - Logging: all lifecycle events enabled
19
+ * - User-Agent: SDK identifier + runtime version + app info + AI agent detection
20
+ * - Request guards: detect misplaced RequestOptions keys in params
21
+ * - Timeout: 60s default (Python overrides to 30s)
22
+ */
23
+ function defaultSdkBehavior() {
24
+ return {
25
+ retry: {
26
+ retryableStatusCodes: [
27
+ 429,
28
+ 500,
29
+ 502,
30
+ 503,
31
+ 504
32
+ ],
33
+ maxRetries: 3,
34
+ retryOnConnectionError: true,
35
+ retryOnTimeout: true,
36
+ backoff: {
37
+ initialDelay: 1,
38
+ multiplier: 2,
39
+ maxDelay: 30,
40
+ jitterFactor: .5
41
+ }
42
+ },
43
+ errors: {
44
+ statusCodeMap: {
45
+ 400: "BadRequest",
46
+ 401: "Authentication",
47
+ 403: "Authorization",
48
+ 404: "NotFound",
49
+ 409: "Conflict",
50
+ 422: "UnprocessableEntity",
51
+ 429: "RateLimitExceeded"
52
+ },
53
+ serverErrorKind: "Server",
54
+ clientErrorKind: "Api",
55
+ errorDocUrlTemplate: "https://workos.com/docs/errors/{code}"
56
+ },
57
+ telemetry: {
58
+ enabledByDefault: true,
59
+ headerName: "X-WorkOS-Client-Telemetry",
60
+ requestIdHeader: "X-Request-ID"
61
+ },
62
+ pagination: { autoPageDelayMs: 0 },
63
+ idempotency: {
64
+ headerName: "Idempotency-Key",
65
+ autoGenerateForPost: true
66
+ },
67
+ logging: {
68
+ enabled: true,
69
+ events: [
70
+ "request.start",
71
+ "request.success",
72
+ "request.retry",
73
+ "request.rate_limited",
74
+ "request.error",
75
+ "request.connection_error"
76
+ ]
77
+ },
78
+ userAgent: {
79
+ sdkIdentifierTemplate: "{name} {lang}/{version}",
80
+ includeRuntimeVersion: true,
81
+ allowAppInfo: true,
82
+ aiAgentEnvVars: [
83
+ {
84
+ envVar: "CLAUDE_CODE",
85
+ agentName: "ClaudeCode"
86
+ },
87
+ {
88
+ envVar: "CURSOR_AGENT",
89
+ agentName: "Cursor"
90
+ },
91
+ {
92
+ envVar: "CLINE_ACTIVE",
93
+ agentName: "Cline"
94
+ },
95
+ {
96
+ envVar: "WINDSURF_ACTIVE",
97
+ agentName: "Windsurf"
98
+ },
99
+ {
100
+ envVar: "COPILOT_AGENT",
101
+ agentName: "Copilot"
102
+ }
103
+ ]
104
+ },
105
+ requestGuard: { optionKeys: [
106
+ "api_key",
107
+ "apiKey",
108
+ "idempotency_key",
109
+ "idempotencyKey",
110
+ "extra_headers",
111
+ "extraHeaders",
112
+ "max_retries",
113
+ "maxRetries",
114
+ "base_url",
115
+ "baseUrl"
116
+ ] },
117
+ timeout: { defaultTimeoutSeconds: 60 }
118
+ };
119
+ }
120
+ //#endregion
7
121
  //#region src/errors.ts
8
122
  /**
9
123
  * Structured error hierarchy for oagen.
@@ -32,13 +146,11 @@ var SpecParseError = class extends OagenError {
32
146
  //#region src/parser/refs.ts
33
147
  async function loadAndBundleSpec(specPath) {
34
148
  const absolutePath = resolve(specPath);
35
- const document = makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href);
36
- const config = await createConfig({});
37
- const resolver = new BaseResolver();
38
149
  const result = await bundleDocument({
39
- document,
40
- config: config.styleguide,
41
- externalRefResolver: resolver,
150
+ document: makeDocumentFromString(await readFile(absolutePath, "utf-8"), pathToFileURL(absolutePath).href),
151
+ config: await createConfig({}),
152
+ types: Oas3_1Types,
153
+ externalRefResolver: new BaseResolver(),
42
154
  dereference: false
43
155
  });
44
156
  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.`);
@@ -107,15 +219,49 @@ function walkTypeRef(ref, visitor) {
107
219
  //#endregion
108
220
  //#region src/utils/naming.ts
109
221
  /**
222
+ * Known compound tokens that the regex-based splitter over-splits.
223
+ * Each entry is [lowercase-word-sequence, canonical-form].
224
+ * Sorted longest-first so greedy matching works correctly.
225
+ */
226
+ const COMPOUND_WORDS = [[[
227
+ "m",
228
+ "2",
229
+ "m"
230
+ ], "M2M"], [["o", "auth"], "OAuth"]];
231
+ /**
232
+ * Recombine adjacent words that form a known compound token.
233
+ */
234
+ function recombineCompounds(words) {
235
+ const result = [];
236
+ let i = 0;
237
+ while (i < words.length) {
238
+ let matched = false;
239
+ for (const [pattern, canonical] of COMPOUND_WORDS) if (i + pattern.length <= words.length) {
240
+ if (pattern.every((p, j) => words[i + j].toLowerCase() === p)) {
241
+ result.push(canonical);
242
+ i += pattern.length;
243
+ matched = true;
244
+ break;
245
+ }
246
+ }
247
+ if (!matched) {
248
+ result.push(words[i]);
249
+ i++;
250
+ }
251
+ }
252
+ return result;
253
+ }
254
+ /**
110
255
  * Split a string into words, handling:
111
256
  * - camelCase / PascalCase boundaries
112
257
  * - snake_case / kebab-case separators
113
258
  * - Consecutive capitals (e.g., "HTTPClient" → ["HTTP", "Client"])
114
259
  * - Numbers as word boundaries (e.g., "OAuth2Token" → ["OAuth", "2", "Token"])
260
+ * - Known compounds are recombined (e.g., "M2M" stays as one word)
115
261
  */
116
262
  function splitWords(s) {
117
263
  if (!s) return [];
118
- return s.replace(/[^a-zA-Z0-9_\-\s.]/g, "_").replace(/([a-z])([A-Z])/g, "$1\0$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2").replace(/([a-zA-Z])(\d)/g, "$1\0$2").replace(/(\d)([a-zA-Z])/g, "$1\0$2").split(/[\0_\-\s.]+/).filter((w) => w.length > 0);
264
+ return recombineCompounds(s.replace(/[^a-zA-Z0-9_\-\s.]/g, "_").replace(/([a-z])([A-Z])/g, "$1\0$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2").replace(/([a-zA-Z])(\d)/g, "$1\0$2").replace(/(\d)([a-zA-Z])/g, "$1\0$2").split(/[\0_\-\s.]+/).filter((w) => w.length > 0));
119
265
  }
120
266
  const ACRONYM_SET = new Set([
121
267
  "SSO",
@@ -123,7 +269,8 @@ const ACRONYM_SET = new Set([
123
269
  "SAML",
124
270
  "SCIM",
125
271
  "JWT",
126
- "HMAC"
272
+ "HMAC",
273
+ "M2M"
127
274
  ]);
128
275
  function toPascalCase(s, acronyms) {
129
276
  const merged = acronyms ? new Set([...ACRONYM_SET, ...acronyms]) : ACRONYM_SET;
@@ -152,11 +299,7 @@ function toSnakeCase(s) {
152
299
  function toUpperSnakeCase(s) {
153
300
  return splitWords(s).map((w) => w.toUpperCase()).join("_");
154
301
  }
155
- const BACKEND_SUFFIXES = [
156
- "Dto",
157
- "DTO",
158
- "Controller"
159
- ];
302
+ const BACKEND_SUFFIXES = ["Controller"];
160
303
  function stripBackendSuffixes(name) {
161
304
  for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
162
305
  return name;
@@ -183,7 +326,9 @@ const SINGULAR_SAFE_LIST = new Set([
183
326
  "Canvas",
184
327
  "Alias",
185
328
  "Basis",
186
- "Bus"
329
+ "Bus",
330
+ "Species",
331
+ "Series"
187
332
  ]);
188
333
  /**
189
334
  * Conservative singularize for the leading PascalCase word.
@@ -286,15 +431,39 @@ function buildInlineModel(name, schema) {
286
431
  }
287
432
  //#endregion
288
433
  //#region src/parser/schemas.ts
289
- 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) {
290
446
  const enums = [];
291
447
  if (!schemas) return {
292
448
  models: [],
293
449
  enums
294
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;
295
464
  const modelsByCleanName = /* @__PURE__ */ new Map();
296
465
  for (const [name, schema] of Object.entries(schemas)) {
297
- const pascalName = cleanSchemaName(toPascalCase(name));
466
+ const pascalName = resolveSchemaName(name);
298
467
  if (schema.enum) enums.push(extractEnum(pascalName, schema));
299
468
  else {
300
469
  const model = extractModel(pascalName, schema, schemas);
@@ -302,6 +471,12 @@ function extractSchemas(schemas) {
302
471
  if (existing) {
303
472
  if (model.fields.length > existing.fields.length) modelsByCleanName.set(pascalName, model);
304
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
+ }
305
480
  }
306
481
  }
307
482
  const models = [...modelsByCleanName.values()];
@@ -317,6 +492,10 @@ function extractSchemas(schemas) {
317
492
  enums
318
493
  };
319
494
  }
495
+ /** Clear the module-level schemaNameTransform. Called by parseSpec() after all extraction phases. */
496
+ function clearSchemaNameTransform() {
497
+ activeSchemaNameTransform = null;
498
+ }
320
499
  /**
321
500
  * Walk a single TypeRef and extract inline enum refs as top-level enum definitions.
322
501
  * Shared by both schemas.ts (field-level) and parse.ts (model-level) collection passes.
@@ -353,7 +532,7 @@ function collectInlineEnums(fields, enums) {
353
532
  function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, models, modelNames, queue) {
354
533
  walkTypeRef(ref, { model: (modelRef) => {
355
534
  if (modelNames.has(modelRef.name)) return;
356
- if (Object.keys(schemas).some((k) => cleanSchemaName(toPascalCase(k)) === modelRef.name)) return;
535
+ if (Object.keys(schemas).some((k) => resolveSchemaName(k) === modelRef.name)) return;
357
536
  const parentSchema = findSchemaByName(parentModelName, schemas);
358
537
  if (!parentSchema) return;
359
538
  const fieldSchema = findNestedFieldSchema(fieldName, parentSchema, schemas);
@@ -368,7 +547,7 @@ function collectNestedInlineModels(ref, fieldName, parentModelName, schemas, mod
368
547
  }
369
548
  /** Find a component schema by PascalCase name. */
370
549
  function findSchemaByName(pascalName, schemas) {
371
- 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;
372
551
  return null;
373
552
  }
374
553
  /** Walk into a schema to find a field's sub-schema, resolving allOf. */
@@ -526,13 +705,88 @@ function extractAllOfModel(name, schema, schemas) {
526
705
  fields
527
706
  };
528
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
+ }
529
783
  function schemaToTypeRef(schema, contextName, parentModelName) {
530
784
  if (schema.$ref) {
531
785
  const segments = schema.$ref.split("/");
532
786
  const rawName = segments[segments.length - 1];
533
787
  return {
534
788
  kind: "model",
535
- name: cleanSchemaName(toPascalCase(rawName))
789
+ name: resolveSchemaName(rawName)
536
790
  };
537
791
  }
538
792
  if (Array.isArray(schema.type)) {
@@ -796,6 +1050,7 @@ const KNOWN_DATA_PATHS = new Set([
796
1050
  "edges"
797
1051
  ]);
798
1052
  const PAGINATION_METADATA_PATTERNS = [
1053
+ "meta",
799
1054
  "metadata",
800
1055
  "pagination",
801
1056
  "cursor",
@@ -827,11 +1082,12 @@ function detectListEnvelope(schema) {
827
1082
  }
828
1083
  if (arrayProps.length === 1 && nonArrayKeys.length >= 1) {
829
1084
  const arrayPropName = arrayProps[0];
830
- 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 {
831
1087
  isEnvelope: true,
832
1088
  dataPath: arrayPropName
833
1089
  };
834
- if (nonArrayKeys.some((key) => PAGINATION_METADATA_PATTERNS.some((pattern) => key.toLowerCase().includes(pattern)))) return {
1090
+ if (hasPaginationCompanion) return {
835
1091
  isEnvelope: true,
836
1092
  dataPath: arrayPropName
837
1093
  };
@@ -1135,13 +1391,33 @@ function extractOperations(paths, operationIdTransform) {
1135
1391
  const op = pathItem[method];
1136
1392
  if (!op) continue;
1137
1393
  const serviceName = inferServiceName(path, op.tags?.[0]);
1138
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform);
1394
+ const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName);
1139
1395
  inlineModels.push(...opModels);
1140
1396
  const ops = serviceMap.get(serviceName) ?? [];
1141
1397
  ops.push(operation);
1142
1398
  serviceMap.set(serviceName, ops);
1143
1399
  }
1144
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
+ }
1145
1421
  for (const [, operations] of serviceMap) disambiguateOperationNames(operations);
1146
1422
  return {
1147
1423
  services: Array.from(serviceMap.entries()).map(([name, operations]) => ({
@@ -1257,13 +1533,14 @@ function inferDeeperContext(path) {
1257
1533
  if (nonParams.length >= 3) return toPascalCase(nonParams.slice(1, -1).join("_"));
1258
1534
  return null;
1259
1535
  }
1260
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform) {
1536
+ function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName) {
1261
1537
  const allParams = [...pathLevelParams, ...op.parameters ?? []];
1262
1538
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
1263
- const pathParams = extractParams(allParams, "path");
1264
- const queryParams = extractParams(allParams, "query");
1265
- const headerParams = extractParams(allParams, "header").filter((p) => p.name.toLowerCase() !== "idempotency-key");
1266
- 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);
1267
1544
  const reqBodyModels = [];
1268
1545
  const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
1269
1546
  const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method);
@@ -1280,6 +1557,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
1280
1557
  itemType
1281
1558
  };
1282
1559
  } else if (paginationFromParams) pagination = paginationFromParams;
1560
+ const security = extractOperationSecurity(op.security);
1283
1561
  return {
1284
1562
  operation: {
1285
1563
  name: inferOperationName(method, path, op.operationId, operationIdTransform),
@@ -1298,19 +1576,35 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform)
1298
1576
  pagination,
1299
1577
  injectIdempotencyKey: hasIdempotencyHeader,
1300
1578
  deprecated: op.deprecated || void 0,
1301
- async: op["x-oagen-async"]
1579
+ async: op["x-oagen-async"],
1580
+ security
1302
1581
  },
1303
1582
  inlineModels
1304
1583
  };
1305
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
+ }
1306
1600
  function buildDescription(summary, description) {
1307
1601
  if (summary && description && summary !== description) return `${summary}\n\n${description}`;
1308
1602
  return description ?? summary;
1309
1603
  }
1310
- function extractParams(params, location) {
1604
+ function extractParams(params, location, operationContext) {
1311
1605
  return params.filter((p) => p.in === location).map((p) => ({
1312
1606
  name: p.name,
1313
- type: p.schema ? schemaToTypeRef(p.schema, p.name) : {
1607
+ type: p.schema ? schemaToTypeRef(p.schema, p.name, operationContext ? toPascalCase(operationContext) : void 0) : {
1314
1608
  kind: "primitive",
1315
1609
  type: "string"
1316
1610
  },
@@ -1496,7 +1790,14 @@ function extractResponses(responses, op, path, method) {
1496
1790
  statusCode: code,
1497
1791
  type: extractedType
1498
1792
  });
1499
- } 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) {
1500
1801
  const jsonContent = resp.content?.["application/json"];
1501
1802
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
1502
1803
  errors.push({
@@ -1623,6 +1924,19 @@ function collectInlineEnumsFromModels(models, enums) {
1623
1924
  const enumNames = new Set(enums.map((e) => e.name));
1624
1925
  for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
1625
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
+ }
1626
1940
  //#endregion
1627
1941
  //#region src/parser/normalize-model-refs.ts
1628
1942
  function validateModelRefs(spec) {
@@ -1652,10 +1966,12 @@ async function parseSpec(specPath, options) {
1652
1966
  const spec = parsed;
1653
1967
  const version = spec.openapi ?? "";
1654
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}\`.`);
1655
- const { models, enums } = extractSchemas(spec.components?.schemas);
1969
+ const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
1656
1970
  const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
1971
+ clearSchemaNameTransform();
1657
1972
  const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
1658
1973
  collectInlineEnumsFromModels(finalModels, enums);
1974
+ collectInlineEnumsFromOperations(services, enums);
1659
1975
  const auth = extractAuthSchemes(spec.components?.securitySchemes);
1660
1976
  const serverEntries = (spec.servers ?? []).map((s) => ({
1661
1977
  url: s.url ?? "",
@@ -1670,7 +1986,8 @@ async function parseSpec(specPath, options) {
1670
1986
  services,
1671
1987
  models: finalModels,
1672
1988
  enums,
1673
- auth
1989
+ auth,
1990
+ sdk: defaultSdkBehavior()
1674
1991
  };
1675
1992
  validateModelRefs(result);
1676
1993
  return result;