@workos/oagen 0.3.0 → 0.5.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 (42) hide show
  1. package/README.md +72 -1
  2. package/dist/cli/index.mjs +955 -90
  3. package/dist/cli/index.mjs.map +1 -1
  4. package/dist/compat/index.d.mts +1 -1
  5. package/dist/compat/index.mjs +9 -4
  6. package/dist/compat/index.mjs.map +1 -1
  7. package/dist/{diff-Cn6S74vV.mjs → diff-CSy3g-Bq.mjs} +541 -43
  8. package/dist/diff-CSy3g-Bq.mjs.map +1 -0
  9. package/dist/{differ-B6472Yef.mjs → differ-CG5ZErKl.mjs} +2 -2
  10. package/dist/{differ-B6472Yef.mjs.map → differ-CG5ZErKl.mjs.map} +1 -1
  11. package/dist/index.d.mts +111 -3
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +340 -30
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/scripts/smoke/baseline.mjs +317 -33
  16. package/dist/scripts/smoke/baseline.mjs.map +1 -1
  17. package/dist/smoke.d.mts +140 -0
  18. package/dist/smoke.d.mts.map +1 -1
  19. package/dist/smoke.mjs +317 -33
  20. package/dist/smoke.mjs.map +1 -1
  21. package/dist/{tree-sitter-DFRXfWg9.mjs → tree-sitter-C53w_0WE.mjs} +5 -7
  22. package/dist/tree-sitter-C53w_0WE.mjs.map +1 -0
  23. package/dist/{types-O25qZxMl.d.mts → types-D84LjrVG.d.mts} +167 -2
  24. package/dist/types-D84LjrVG.d.mts.map +1 -0
  25. package/dist/verify/index.d.mts +1 -1
  26. package/dist/verify/index.mjs +4 -4
  27. package/dist/verify/index.mjs.map +1 -1
  28. package/docs/agents/architecture.md +1 -1
  29. package/docs/architecture/emitter-contract.md +1 -2
  30. package/docs/architecture/ir-types.md +73 -1
  31. package/docs/architecture/pipeline.md +0 -1
  32. package/docs/cli.md +29 -8
  33. package/docs/core/minimal-emitter.md +0 -4
  34. package/docs/core/quickstart.md +0 -1
  35. package/examples/reference-emitter/README.md +0 -1
  36. package/examples/reference-emitter/src/emitter.ts +0 -5
  37. package/package.json +4 -4
  38. package/skills/review-operations/SKILL.md +83 -0
  39. package/dist/diff-Cn6S74vV.mjs.map +0 -1
  40. package/dist/tree-sitter-DFRXfWg9.mjs.map +0 -1
  41. package/dist/types-O25qZxMl.d.mts.map +0 -1
  42. package/examples/reference-emitter/src/generators/config.ts +0 -19
@@ -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.`);
@@ -187,11 +299,7 @@ function toSnakeCase(s) {
187
299
  function toUpperSnakeCase(s) {
188
300
  return splitWords(s).map((w) => w.toUpperCase()).join("_");
189
301
  }
190
- const BACKEND_SUFFIXES = [
191
- "Dto",
192
- "DTO",
193
- "Controller"
194
- ];
302
+ const BACKEND_SUFFIXES = ["Controller"];
195
303
  function stripBackendSuffixes(name) {
196
304
  for (const suffix of BACKEND_SUFFIXES) if (name.endsWith(suffix) && name.length > suffix.length) return name.slice(0, -suffix.length);
197
305
  return name;
@@ -218,7 +326,9 @@ const SINGULAR_SAFE_LIST = new Set([
218
326
  "Canvas",
219
327
  "Alias",
220
328
  "Basis",
221
- "Bus"
329
+ "Bus",
330
+ "Species",
331
+ "Series"
222
332
  ]);
223
333
  /**
224
334
  * Conservative singularize for the leading PascalCase word.
@@ -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
  },
@@ -1353,7 +1612,9 @@ function extractParams(params, location) {
1353
1612
  description: p.description,
1354
1613
  deprecated: p.deprecated || p.schema?.deprecated || void 0,
1355
1614
  default: p.schema?.default,
1356
- example: p.example ?? p.schema?.example
1615
+ example: p.example ?? p.schema?.example,
1616
+ style: p.style,
1617
+ explode: p.explode
1357
1618
  }));
1358
1619
  }
1359
1620
  function extractRequestBody(body, op, inlineModels) {
@@ -1531,7 +1792,14 @@ function extractResponses(responses, op, path, method) {
1531
1792
  statusCode: code,
1532
1793
  type: extractedType
1533
1794
  });
1534
- } else if (code >= 400) {
1795
+ } else if (code >= 300 && code < 400) allSuccessResponses.push({
1796
+ statusCode: code,
1797
+ type: {
1798
+ kind: "primitive",
1799
+ type: "unknown"
1800
+ }
1801
+ });
1802
+ else if (code >= 400) {
1535
1803
  const jsonContent = resp.content?.["application/json"];
1536
1804
  const type = jsonContent?.schema ? schemaToTypeRef(jsonContent.schema, `Error${code}`) : void 0;
1537
1805
  errors.push({
@@ -1658,6 +1926,19 @@ function collectInlineEnumsFromModels(models, enums) {
1658
1926
  const enumNames = new Set(enums.map((e) => e.name));
1659
1927
  for (const model of models) for (const field of model.fields) collectInlineEnumFromRef(field.type, enums, enumNames);
1660
1928
  }
1929
+ /**
1930
+ * Collect inline enum definitions from operation parameters (query, path, header)
1931
+ * and promote them to top-level enums so emitters can generate typed enum files.
1932
+ */
1933
+ function collectInlineEnumsFromOperations(services, enums) {
1934
+ const enumNames = new Set(enums.map((e) => e.name));
1935
+ for (const service of services) for (const op of service.operations) for (const param of [
1936
+ ...op.pathParams,
1937
+ ...op.queryParams,
1938
+ ...op.headerParams,
1939
+ ...op.cookieParams ?? []
1940
+ ]) collectInlineEnumFromRef(param.type, enums, enumNames);
1941
+ }
1661
1942
  //#endregion
1662
1943
  //#region src/parser/normalize-model-refs.ts
1663
1944
  function validateModelRefs(spec) {
@@ -1687,10 +1968,12 @@ async function parseSpec(specPath, options) {
1687
1968
  const spec = parsed;
1688
1969
  const version = spec.openapi ?? "";
1689
1970
  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);
1971
+ const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
1691
1972
  const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform);
1973
+ clearSchemaNameTransform();
1692
1974
  const finalModels = collapseJsonSuffixModels(mergeFieldInlineModels(mergeInlineResponseModels(models, inlineModels), extractInlineModelsFromSchemas(spec.components?.schemas)), services);
1693
1975
  collectInlineEnumsFromModels(finalModels, enums);
1976
+ collectInlineEnumsFromOperations(services, enums);
1694
1977
  const auth = extractAuthSchemes(spec.components?.securitySchemes);
1695
1978
  const serverEntries = (spec.servers ?? []).map((s) => ({
1696
1979
  url: s.url ?? "",
@@ -1705,7 +1988,8 @@ async function parseSpec(specPath, options) {
1705
1988
  services,
1706
1989
  models: finalModels,
1707
1990
  enums,
1708
- auth
1991
+ auth,
1992
+ sdk: defaultSdkBehavior()
1709
1993
  };
1710
1994
  validateModelRefs(result);
1711
1995
  return result;