@workos/oagen 0.30.0 → 0.30.2

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.
@@ -2037,6 +2037,27 @@ function resolveParameterRef(raw, componentParameters, operationContext) {
2037
2037
  }
2038
2038
  return current;
2039
2039
  }
2040
+ /**
2041
+ * Resolve a raw request body to a concrete Request Body Object, following
2042
+ * `$ref` chains into `components/requestBodies` with a cycle guard. Throws on
2043
+ * an unresolvable ref so a malformed spec fails at parse time instead of
2044
+ * silently losing the body — and with it the body-ownership rule that keeps
2045
+ * duplicated secrets out of the query string.
2046
+ */
2047
+ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
2048
+ if (!raw) return void 0;
2049
+ const seenRefs = /* @__PURE__ */ new Set();
2050
+ let current = raw;
2051
+ while ("$ref" in current) {
2052
+ if (seenRefs.has(current.$ref)) throw new Error(`Circular request body $ref chain starting at "${current.$ref}" in ${operationContext}.`);
2053
+ seenRefs.add(current.$ref);
2054
+ const refName = /^#\/components\/requestBodies\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
2055
+ const resolved = refName ? componentRequestBodies?.[refName] : void 0;
2056
+ if (!resolved) throw new Error(`Unresolved request body $ref "${current.$ref}" in ${operationContext}: no matching entry under components/requestBodies.`);
2057
+ current = resolved;
2058
+ }
2059
+ return current;
2060
+ }
2040
2061
  const HTTP_METHODS = [
2041
2062
  "get",
2042
2063
  "post",
@@ -2047,7 +2068,7 @@ const HTTP_METHODS = [
2047
2068
  "options",
2048
2069
  "trace"
2049
2070
  ];
2050
- function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
2071
+ function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies) {
2051
2072
  if (!paths) return {
2052
2073
  services: [],
2053
2074
  inlineModels: []
@@ -2060,7 +2081,7 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
2060
2081
  const op = pathItem[method];
2061
2082
  if (!op) continue;
2062
2083
  const serviceName = inferServiceName(path, op.tags?.[0]);
2063
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters);
2084
+ const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies);
2064
2085
  inlineModels.push(...opModels);
2065
2086
  const ops = serviceMap.get(serviceName) ?? [];
2066
2087
  ops.push(operation);
@@ -2258,10 +2279,10 @@ function extractParameterGroups(op, allIRParams, operationContext) {
2258
2279
  * the request body. Emitters use the group's presence to generate sum-type
2259
2280
  * interfaces and custom JSON marshalling instead of flat optional fields.
2260
2281
  */
2261
- function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2282
+ function extractBodyParameterGroups(op, requestBody, operationContext, componentSchemas) {
2262
2283
  const raw = op["x-mutually-exclusive-body-groups"];
2263
2284
  if (!raw || typeof raw !== "object") return void 0;
2264
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
2285
+ const bodySchema = requestBody?.content?.["application/json"]?.schema;
2265
2286
  const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
2266
2287
  const groups = [];
2267
2288
  for (const [groupName, groupDef] of Object.entries(raw)) {
@@ -2301,8 +2322,9 @@ function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2301
2322
  }
2302
2323
  return groups.length > 0 ? groups : void 0;
2303
2324
  }
2304
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
2325
+ function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies) {
2305
2326
  const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
2327
+ const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
2306
2328
  const paramsByIdentity = /* @__PURE__ */ new Map();
2307
2329
  for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
2308
2330
  const param = resolveParameterRef(raw, componentParameters, opLabel);
@@ -2310,13 +2332,21 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2310
2332
  }
2311
2333
  const allParams = [...paramsByIdentity.values()];
2312
2334
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
2335
+ const groupedQueryNames = collectParameterGroupMemberNames(op);
2336
+ const bodyOwnedNames = /* @__PURE__ */ new Set();
2337
+ if (BODY_CARRYING_METHODS.has(method)) {
2338
+ const bodySchema = selectRequestBodyContent(requestBodyObject).schema;
2339
+ for (const name of collectBodyPropertyNames(bodySchema, componentSchemas)) if (!groupedQueryNames.has(name)) bodyOwnedNames.add(name);
2340
+ }
2341
+ const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
2313
2342
  const opContext = serviceName;
2314
- const pathParams = extractParams(allParams, "path", opContext, componentSchemas);
2315
- const queryParams = extractParams(allParams, "query", opContext, componentSchemas);
2316
- const headerParams = extractParams(allParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2317
- const cookieParams = extractParams(allParams, "cookie", opContext, componentSchemas);
2343
+ const pathParams = extractParams(wireParams, "path", opContext, componentSchemas);
2344
+ const queryParams = extractParams(wireParams, "query", opContext, componentSchemas);
2345
+ const headerParams = extractParams(wireParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2346
+ const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas);
2347
+ const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas) : [];
2318
2348
  const reqBodyModels = [];
2319
- const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
2349
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, reqBodyModels);
2320
2350
  const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas);
2321
2351
  inlineModels.push(...reqBodyModels);
2322
2352
  const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
@@ -2338,7 +2368,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2338
2368
  ...headerParams,
2339
2369
  ...cookieParams
2340
2370
  ], opLabel);
2341
- const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
2371
+ const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
2342
2372
  const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
2343
2373
  return {
2344
2374
  operation: {
@@ -2348,6 +2378,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2348
2378
  path,
2349
2379
  pathParams,
2350
2380
  queryParams,
2381
+ bodyOwnedQueryParams: bodyOwnedQueryParams.length > 0 ? bodyOwnedQueryParams : void 0,
2351
2382
  headerParams,
2352
2383
  cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
2353
2384
  requestBody,
@@ -2470,26 +2501,91 @@ function resolveSchemaRefObject(schema, componentSchemas) {
2470
2501
  if (!match) return void 0;
2471
2502
  return componentSchemas[match[1]];
2472
2503
  }
2504
+ /** HTTP methods whose request body is the primary parameter carrier. */
2505
+ const BODY_CARRYING_METHODS = new Set([
2506
+ "post",
2507
+ "put",
2508
+ "patch"
2509
+ ]);
2510
+ /**
2511
+ * Pick the request body's schema and wire encoding from its content map, in
2512
+ * priority order. JSON wins over form encodings; binary and text bodies may
2513
+ * legitimately carry no schema at all.
2514
+ */
2515
+ function selectRequestBodyContent(body) {
2516
+ const content = body?.content;
2517
+ if (!content) return { encoding: "json" };
2518
+ if (content["application/json"]?.schema) return {
2519
+ encoding: "json",
2520
+ schema: content["application/json"].schema
2521
+ };
2522
+ if (content["multipart/form-data"]?.schema) return {
2523
+ encoding: "form-data",
2524
+ schema: content["multipart/form-data"].schema
2525
+ };
2526
+ if (content["application/x-www-form-urlencoded"]?.schema) return {
2527
+ encoding: "form-urlencoded",
2528
+ schema: content["application/x-www-form-urlencoded"].schema
2529
+ };
2530
+ if (content["application/octet-stream"]) return {
2531
+ encoding: "binary",
2532
+ schema: content["application/octet-stream"].schema
2533
+ };
2534
+ if (content["text/plain"]) return {
2535
+ encoding: "text",
2536
+ schema: content["text/plain"].schema
2537
+ };
2538
+ return { encoding: "json" };
2539
+ }
2540
+ /** Parameter names referenced by any `x-mutually-exclusive-parameter-groups` variant. */
2541
+ function collectParameterGroupMemberNames(op) {
2542
+ const names = /* @__PURE__ */ new Set();
2543
+ const raw = op["x-mutually-exclusive-parameter-groups"];
2544
+ if (!raw || typeof raw !== "object") return names;
2545
+ for (const groupDef of Object.values(raw)) {
2546
+ const variants = groupDef && typeof groupDef === "object" ? groupDef.variants : void 0;
2547
+ if (!variants || typeof variants !== "object") continue;
2548
+ for (const members of Object.values(variants)) {
2549
+ if (!Array.isArray(members)) continue;
2550
+ for (const member of members) if (typeof member === "string") names.add(member);
2551
+ }
2552
+ }
2553
+ return names;
2554
+ }
2555
+ /**
2556
+ * Every property name a request body is guaranteed to accept, following
2557
+ * `$ref`s and descending through composition. `allOf` members all apply, so
2558
+ * their properties union. `oneOf` / `anyOf` alternatives are choices, so only
2559
+ * a property every non-null alternative declares counts — otherwise a caller
2560
+ * on another branch would have no slot for the value in either the body or
2561
+ * the query. Used to decide which `in: query` declarations are body-owned.
2562
+ */
2563
+ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__ */ new Set()) {
2564
+ if (!schema) return /* @__PURE__ */ new Set();
2565
+ const ref = schema.$ref;
2566
+ if (ref) {
2567
+ if (seen.has(ref)) return /* @__PURE__ */ new Set();
2568
+ return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, new Set([...seen, ref]));
2569
+ }
2570
+ const names = new Set(Object.keys(schema.properties ?? {}));
2571
+ for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
2572
+ for (const alternatives of [schema.oneOf, schema.anyOf]) {
2573
+ const branches = (alternatives ?? []).filter((alt) => alt.type !== "null");
2574
+ if (branches.length === 0) continue;
2575
+ let common;
2576
+ for (const branch of branches) {
2577
+ const branchNames = collectBodyPropertyNames(branch, componentSchemas, seen);
2578
+ common = common ? new Set([...common].filter((name) => branchNames.has(name))) : branchNames;
2579
+ }
2580
+ for (const name of common ?? []) names.add(name);
2581
+ }
2582
+ return names;
2583
+ }
2473
2584
  function extractRequestBody(body, op, inlineModels) {
2474
2585
  if (!body?.content) return {};
2475
- let encoding = "json";
2476
- let schema;
2477
- if (body.content["application/json"]?.schema) {
2478
- encoding = "json";
2479
- schema = body.content["application/json"].schema;
2480
- } else if (body.content["multipart/form-data"]?.schema) {
2481
- encoding = "form-data";
2482
- schema = body.content["multipart/form-data"].schema;
2483
- } else if (body.content["application/x-www-form-urlencoded"]?.schema) {
2484
- encoding = "form-urlencoded";
2485
- schema = body.content["application/x-www-form-urlencoded"].schema;
2486
- } else if (body.content["application/octet-stream"]) {
2487
- encoding = "binary";
2488
- schema = body.content["application/octet-stream"].schema;
2489
- } else if (body.content["text/plain"]) {
2490
- encoding = "text";
2491
- schema = body.content["text/plain"].schema;
2492
- }
2586
+ const selected = selectRequestBodyContent(body);
2587
+ const encoding = selected.encoding;
2588
+ let schema = selected.schema;
2493
2589
  if (!schema) {
2494
2590
  if (encoding === "binary") return {
2495
2591
  body: {
@@ -2877,6 +2973,7 @@ function collectInlineEnumsFromOperations(services, enums) {
2877
2973
  for (const service of services) for (const op of service.operations) for (const param of [
2878
2974
  ...op.pathParams,
2879
2975
  ...op.queryParams,
2976
+ ...op.bodyOwnedQueryParams ?? [],
2880
2977
  ...op.headerParams,
2881
2978
  ...op.cookieParams ?? []
2882
2979
  ]) collectInlineEnumFromRef(param.type, enums, enumNames);
@@ -3153,6 +3250,7 @@ function validateModelRefs(spec) {
3153
3250
  for (const p of [
3154
3251
  ...op.pathParams,
3155
3252
  ...op.queryParams,
3253
+ ...op.bodyOwnedQueryParams ?? [],
3156
3254
  ...op.headerParams
3157
3255
  ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
3158
3256
  if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
@@ -3167,7 +3265,7 @@ async function parseSpec(specPath, options) {
3167
3265
  const version = spec.openapi ?? "";
3168
3266
  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}\`.`);
3169
3267
  const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
3170
- const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
3268
+ const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies);
3171
3269
  const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
3172
3270
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
3173
3271
  clearSchemaNameTransform();
@@ -6529,6 +6627,7 @@ function collectReferencedNames(services, models, opts = {}) {
6529
6627
  for (const p of [
6530
6628
  ...op.pathParams,
6531
6629
  ...op.queryParams,
6630
+ ...op.bodyOwnedQueryParams ?? [],
6532
6631
  ...op.headerParams,
6533
6632
  ...op.cookieParams ?? []
6534
6633
  ]) collectFromTypeRef(p.type);
@@ -7793,6 +7892,10 @@ function expandDocUrls(spec, docUrl) {
7793
7892
  ...p,
7794
7893
  description: expand(p.description)
7795
7894
  })),
7895
+ bodyOwnedQueryParams: op.bodyOwnedQueryParams?.map((p) => ({
7896
+ ...p,
7897
+ description: expand(p.description)
7898
+ })),
7796
7899
  headerParams: op.headerParams.map((p) => ({
7797
7900
  ...p,
7798
7901
  description: expand(p.description)
@@ -7886,6 +7989,7 @@ function buildDirectionIndex(...specs) {
7886
7989
  for (const param of [
7887
7990
  ...op.pathParams,
7888
7991
  ...op.queryParams,
7992
+ ...op.bodyOwnedQueryParams ?? [],
7889
7993
  ...op.headerParams,
7890
7994
  ...op.cookieParams ?? []
7891
7995
  ]) requestSeeds.push(param.type);
@@ -9229,7 +9333,8 @@ function classifyParameterChanges(baseline, candidate, policy, specRef, language
9229
9333
  old: { parameter: "(absent)" },
9230
9334
  new: {
9231
9335
  parameter: candParam.publicName,
9232
- required: "true"
9336
+ required: "true",
9337
+ position: String(candParam.position)
9233
9338
  },
9234
9339
  message: `Required parameter "${candParam.publicName}" added to "${baseline.displayName}"`,
9235
9340
  policy,
@@ -9239,7 +9344,10 @@ function classifyParameterChanges(baseline, candidate, policy, specRef, language
9239
9344
  category,
9240
9345
  symbol: baseline.fqName,
9241
9346
  old: { parameter: "(absent)" },
9242
- new: { parameter: candParam.publicName },
9347
+ new: {
9348
+ parameter: candParam.publicName,
9349
+ position: String(candParam.position)
9350
+ },
9243
9351
  message: `Optional parameter "${candParam.publicName}" added to "${baseline.displayName}"`,
9244
9352
  policy,
9245
9353
  specRef
@@ -9329,6 +9437,7 @@ function specDerivedNames(spec, hints) {
9329
9437
  for (const p of [
9330
9438
  ...op.pathParams,
9331
9439
  ...op.queryParams,
9440
+ ...op.bodyOwnedQueryParams ?? [],
9332
9441
  ...op.headerParams,
9333
9442
  ...op.cookieParams ?? []
9334
9443
  ]) collectTypeRefNames(p.type, names, hints, topLevelEnumNames);