@workos/oagen 0.30.1 → 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.
@@ -1964,6 +1964,27 @@ function resolveParameterRef(raw, componentParameters, operationContext) {
1964
1964
  }
1965
1965
  return current;
1966
1966
  }
1967
+ /**
1968
+ * Resolve a raw request body to a concrete Request Body Object, following
1969
+ * `$ref` chains into `components/requestBodies` with a cycle guard. Throws on
1970
+ * an unresolvable ref so a malformed spec fails at parse time instead of
1971
+ * silently losing the body — and with it the body-ownership rule that keeps
1972
+ * duplicated secrets out of the query string.
1973
+ */
1974
+ function resolveRequestBodyRef(raw, componentRequestBodies, operationContext) {
1975
+ if (!raw) return void 0;
1976
+ const seenRefs = /* @__PURE__ */ new Set();
1977
+ let current = raw;
1978
+ while ("$ref" in current) {
1979
+ if (seenRefs.has(current.$ref)) throw new Error(`Circular request body $ref chain starting at "${current.$ref}" in ${operationContext}.`);
1980
+ seenRefs.add(current.$ref);
1981
+ const refName = /^#\/components\/requestBodies\/(.+)$/.exec(current.$ref)?.[1].replace(/~1/g, "/").replace(/~0/g, "~");
1982
+ const resolved = refName ? componentRequestBodies?.[refName] : void 0;
1983
+ if (!resolved) throw new Error(`Unresolved request body $ref "${current.$ref}" in ${operationContext}: no matching entry under components/requestBodies.`);
1984
+ current = resolved;
1985
+ }
1986
+ return current;
1987
+ }
1967
1988
  const HTTP_METHODS = [
1968
1989
  "get",
1969
1990
  "post",
@@ -1974,7 +1995,7 @@ const HTTP_METHODS = [
1974
1995
  "options",
1975
1996
  "trace"
1976
1997
  ];
1977
- function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters) {
1998
+ function extractOperations(paths, operationIdTransform, componentSchemas, componentParameters, componentRequestBodies) {
1978
1999
  if (!paths) return {
1979
2000
  services: [],
1980
2001
  inlineModels: []
@@ -1987,7 +2008,7 @@ function extractOperations(paths, operationIdTransform, componentSchemas, compon
1987
2008
  const op = pathItem[method];
1988
2009
  if (!op) continue;
1989
2010
  const serviceName = inferServiceName(path, op.tags?.[0]);
1990
- const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters);
2011
+ const { operation, inlineModels: opModels } = buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies);
1991
2012
  inlineModels.push(...opModels);
1992
2013
  const ops = serviceMap.get(serviceName) ?? [];
1993
2014
  ops.push(operation);
@@ -2185,10 +2206,10 @@ function extractParameterGroups(op, allIRParams, operationContext) {
2185
2206
  * the request body. Emitters use the group's presence to generate sum-type
2186
2207
  * interfaces and custom JSON marshalling instead of flat optional fields.
2187
2208
  */
2188
- function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2209
+ function extractBodyParameterGroups(op, requestBody, operationContext, componentSchemas) {
2189
2210
  const raw = op["x-mutually-exclusive-body-groups"];
2190
2211
  if (!raw || typeof raw !== "object") return void 0;
2191
- const bodySchema = op.requestBody?.content?.["application/json"]?.schema;
2212
+ const bodySchema = requestBody?.content?.["application/json"]?.schema;
2192
2213
  const variantFieldSchemas = collectVariantFieldSchemas(bodySchema, componentSchemas);
2193
2214
  const groups = [];
2194
2215
  for (const [groupName, groupDef] of Object.entries(raw)) {
@@ -2228,8 +2249,9 @@ function extractBodyParameterGroups(op, operationContext, componentSchemas) {
2228
2249
  }
2229
2250
  return groups.length > 0 ? groups : void 0;
2230
2251
  }
2231
- function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters) {
2252
+ function buildOperation(method, path, op, pathLevelParams, operationIdTransform, serviceName, componentSchemas, componentParameters, componentRequestBodies) {
2232
2253
  const opLabel = op.operationId ?? `${method.toUpperCase()} ${path}`;
2254
+ const requestBodyObject = resolveRequestBodyRef(op.requestBody, componentRequestBodies, opLabel);
2233
2255
  const paramsByIdentity = /* @__PURE__ */ new Map();
2234
2256
  for (const raw of [...pathLevelParams, ...op.parameters ?? []]) {
2235
2257
  const param = resolveParameterRef(raw, componentParameters, opLabel);
@@ -2237,13 +2259,21 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2237
2259
  }
2238
2260
  const allParams = [...paramsByIdentity.values()];
2239
2261
  const hasIdempotencyHeader = allParams.some((p) => p.in === "header" && p.name.toLowerCase() === "idempotency-key");
2262
+ const groupedQueryNames = collectParameterGroupMemberNames(op);
2263
+ const bodyOwnedNames = /* @__PURE__ */ new Set();
2264
+ if (BODY_CARRYING_METHODS.has(method)) {
2265
+ const bodySchema = selectRequestBodyContent(requestBodyObject).schema;
2266
+ for (const name of collectBodyPropertyNames(bodySchema, componentSchemas)) if (!groupedQueryNames.has(name)) bodyOwnedNames.add(name);
2267
+ }
2268
+ const wireParams = bodyOwnedNames.size > 0 ? allParams.filter((p) => !(p.in === "query" && bodyOwnedNames.has(p.name))) : allParams;
2240
2269
  const opContext = serviceName;
2241
- const pathParams = extractParams(allParams, "path", opContext, componentSchemas);
2242
- const queryParams = extractParams(allParams, "query", opContext, componentSchemas);
2243
- const headerParams = extractParams(allParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2244
- const cookieParams = extractParams(allParams, "cookie", opContext, componentSchemas);
2270
+ const pathParams = extractParams(wireParams, "path", opContext, componentSchemas);
2271
+ const queryParams = extractParams(wireParams, "query", opContext, componentSchemas);
2272
+ const headerParams = extractParams(wireParams, "header", opContext, componentSchemas).filter((p) => p.name.toLowerCase() !== "idempotency-key");
2273
+ const cookieParams = extractParams(wireParams, "cookie", opContext, componentSchemas);
2274
+ const bodyOwnedQueryParams = bodyOwnedNames.size > 0 ? extractParams(allParams.filter((p) => p.in === "query" && bodyOwnedNames.has(p.name)), "query", opContext, componentSchemas) : [];
2245
2275
  const reqBodyModels = [];
2246
- const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(op.requestBody, op, reqBodyModels);
2276
+ const { body: requestBody, encoding: requestBodyEncoding } = extractRequestBody(requestBodyObject, op, reqBodyModels);
2247
2277
  const { response, successResponses, errors, inlineModels, isPaginated, dataPath: responseDataPath, itemType: responseItemType } = extractResponses(op.responses, op, path, method, componentSchemas);
2248
2278
  inlineModels.push(...reqBodyModels);
2249
2279
  const paginationFromParams = detectPagination(response, queryParams, responseDataPath);
@@ -2265,7 +2295,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2265
2295
  ...headerParams,
2266
2296
  ...cookieParams
2267
2297
  ], opLabel);
2268
- const bodyParamGroups = extractBodyParameterGroups(op, opLabel, componentSchemas);
2298
+ const bodyParamGroups = extractBodyParameterGroups(op, requestBodyObject, opLabel, componentSchemas);
2269
2299
  const parameterGroups = queryParamGroups || bodyParamGroups ? [...queryParamGroups ?? [], ...bodyParamGroups ?? []] : void 0;
2270
2300
  return {
2271
2301
  operation: {
@@ -2275,6 +2305,7 @@ function buildOperation(method, path, op, pathLevelParams, operationIdTransform,
2275
2305
  path,
2276
2306
  pathParams,
2277
2307
  queryParams,
2308
+ bodyOwnedQueryParams: bodyOwnedQueryParams.length > 0 ? bodyOwnedQueryParams : void 0,
2278
2309
  headerParams,
2279
2310
  cookieParams: cookieParams.length > 0 ? cookieParams : void 0,
2280
2311
  requestBody,
@@ -2397,26 +2428,91 @@ function resolveSchemaRefObject(schema, componentSchemas) {
2397
2428
  if (!match) return void 0;
2398
2429
  return componentSchemas[match[1]];
2399
2430
  }
2431
+ /** HTTP methods whose request body is the primary parameter carrier. */
2432
+ const BODY_CARRYING_METHODS = new Set([
2433
+ "post",
2434
+ "put",
2435
+ "patch"
2436
+ ]);
2437
+ /**
2438
+ * Pick the request body's schema and wire encoding from its content map, in
2439
+ * priority order. JSON wins over form encodings; binary and text bodies may
2440
+ * legitimately carry no schema at all.
2441
+ */
2442
+ function selectRequestBodyContent(body) {
2443
+ const content = body?.content;
2444
+ if (!content) return { encoding: "json" };
2445
+ if (content["application/json"]?.schema) return {
2446
+ encoding: "json",
2447
+ schema: content["application/json"].schema
2448
+ };
2449
+ if (content["multipart/form-data"]?.schema) return {
2450
+ encoding: "form-data",
2451
+ schema: content["multipart/form-data"].schema
2452
+ };
2453
+ if (content["application/x-www-form-urlencoded"]?.schema) return {
2454
+ encoding: "form-urlencoded",
2455
+ schema: content["application/x-www-form-urlencoded"].schema
2456
+ };
2457
+ if (content["application/octet-stream"]) return {
2458
+ encoding: "binary",
2459
+ schema: content["application/octet-stream"].schema
2460
+ };
2461
+ if (content["text/plain"]) return {
2462
+ encoding: "text",
2463
+ schema: content["text/plain"].schema
2464
+ };
2465
+ return { encoding: "json" };
2466
+ }
2467
+ /** Parameter names referenced by any `x-mutually-exclusive-parameter-groups` variant. */
2468
+ function collectParameterGroupMemberNames(op) {
2469
+ const names = /* @__PURE__ */ new Set();
2470
+ const raw = op["x-mutually-exclusive-parameter-groups"];
2471
+ if (!raw || typeof raw !== "object") return names;
2472
+ for (const groupDef of Object.values(raw)) {
2473
+ const variants = groupDef && typeof groupDef === "object" ? groupDef.variants : void 0;
2474
+ if (!variants || typeof variants !== "object") continue;
2475
+ for (const members of Object.values(variants)) {
2476
+ if (!Array.isArray(members)) continue;
2477
+ for (const member of members) if (typeof member === "string") names.add(member);
2478
+ }
2479
+ }
2480
+ return names;
2481
+ }
2482
+ /**
2483
+ * Every property name a request body is guaranteed to accept, following
2484
+ * `$ref`s and descending through composition. `allOf` members all apply, so
2485
+ * their properties union. `oneOf` / `anyOf` alternatives are choices, so only
2486
+ * a property every non-null alternative declares counts — otherwise a caller
2487
+ * on another branch would have no slot for the value in either the body or
2488
+ * the query. Used to decide which `in: query` declarations are body-owned.
2489
+ */
2490
+ function collectBodyPropertyNames(schema, componentSchemas, seen = /* @__PURE__ */ new Set()) {
2491
+ if (!schema) return /* @__PURE__ */ new Set();
2492
+ const ref = schema.$ref;
2493
+ if (ref) {
2494
+ if (seen.has(ref)) return /* @__PURE__ */ new Set();
2495
+ return collectBodyPropertyNames(resolveSchemaRefObject(schema, componentSchemas), componentSchemas, new Set([...seen, ref]));
2496
+ }
2497
+ const names = new Set(Object.keys(schema.properties ?? {}));
2498
+ for (const sub of schema.allOf ?? []) for (const name of collectBodyPropertyNames(sub, componentSchemas, seen)) names.add(name);
2499
+ for (const alternatives of [schema.oneOf, schema.anyOf]) {
2500
+ const branches = (alternatives ?? []).filter((alt) => alt.type !== "null");
2501
+ if (branches.length === 0) continue;
2502
+ let common;
2503
+ for (const branch of branches) {
2504
+ const branchNames = collectBodyPropertyNames(branch, componentSchemas, seen);
2505
+ common = common ? new Set([...common].filter((name) => branchNames.has(name))) : branchNames;
2506
+ }
2507
+ for (const name of common ?? []) names.add(name);
2508
+ }
2509
+ return names;
2510
+ }
2400
2511
  function extractRequestBody(body, op, inlineModels) {
2401
2512
  if (!body?.content) return {};
2402
- let encoding = "json";
2403
- let schema;
2404
- if (body.content["application/json"]?.schema) {
2405
- encoding = "json";
2406
- schema = body.content["application/json"].schema;
2407
- } else if (body.content["multipart/form-data"]?.schema) {
2408
- encoding = "form-data";
2409
- schema = body.content["multipart/form-data"].schema;
2410
- } else if (body.content["application/x-www-form-urlencoded"]?.schema) {
2411
- encoding = "form-urlencoded";
2412
- schema = body.content["application/x-www-form-urlencoded"].schema;
2413
- } else if (body.content["application/octet-stream"]) {
2414
- encoding = "binary";
2415
- schema = body.content["application/octet-stream"].schema;
2416
- } else if (body.content["text/plain"]) {
2417
- encoding = "text";
2418
- schema = body.content["text/plain"].schema;
2419
- }
2513
+ const selected = selectRequestBodyContent(body);
2514
+ const encoding = selected.encoding;
2515
+ let schema = selected.schema;
2420
2516
  if (!schema) {
2421
2517
  if (encoding === "binary") return {
2422
2518
  body: {
@@ -2804,6 +2900,7 @@ function collectInlineEnumsFromOperations(services, enums) {
2804
2900
  for (const service of services) for (const op of service.operations) for (const param of [
2805
2901
  ...op.pathParams,
2806
2902
  ...op.queryParams,
2903
+ ...op.bodyOwnedQueryParams ?? [],
2807
2904
  ...op.headerParams,
2808
2905
  ...op.cookieParams ?? []
2809
2906
  ]) collectInlineEnumFromRef(param.type, enums, enumNames);
@@ -3080,6 +3177,7 @@ function validateModelRefs(spec) {
3080
3177
  for (const p of [
3081
3178
  ...op.pathParams,
3082
3179
  ...op.queryParams,
3180
+ ...op.bodyOwnedQueryParams ?? [],
3083
3181
  ...op.headerParams
3084
3182
  ]) walkRef(p.type, `${service.name}.${op.name}.${p.name}`);
3085
3183
  if (op.requestBody) walkRef(op.requestBody, `${service.name}.${op.name}.requestBody`);
@@ -3094,7 +3192,7 @@ async function parseSpec(specPath, options) {
3094
3192
  const version = spec.openapi ?? "";
3095
3193
  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}\`.`);
3096
3194
  const { models, enums } = extractSchemas(spec.components?.schemas, { schemaNameTransform: options?.schemaNameTransform });
3097
- const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters);
3195
+ const { services, inlineModels } = extractOperations(spec.paths, options?.operationIdTransform, spec.components?.schemas, spec.components?.parameters, spec.components?.requestBodies);
3098
3196
  const responseNormalizedModels = mergeInlineResponseModels(models, inlineModels);
3099
3197
  const fieldInlineModels = extractInlineModelsFromSchemas(spec.components?.schemas);
3100
3198
  clearSchemaNameTransform();