@workos/oagen-emitters 0.12.1 → 0.12.3

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 (45) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint-pr-title.yml +1 -1
  3. package/.github/workflows/lint.yml +1 -1
  4. package/.github/workflows/release-please.yml +2 -2
  5. package/.github/workflows/release.yml +1 -1
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +14 -0
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{plugin-CmfzawTp.mjs → plugin-D2N2ZT5W.mjs} +2566 -1493
  12. package/dist/plugin-D2N2ZT5W.mjs.map +1 -0
  13. package/dist/plugin.mjs +1 -1
  14. package/package.json +6 -6
  15. package/renovate.json +46 -6
  16. package/src/node/client.ts +19 -32
  17. package/src/node/enums.ts +67 -30
  18. package/src/node/errors.ts +2 -8
  19. package/src/node/field-plan.ts +188 -52
  20. package/src/node/fixtures.ts +11 -33
  21. package/src/node/index.ts +354 -20
  22. package/src/node/live-surface.ts +378 -0
  23. package/src/node/models.ts +547 -351
  24. package/src/node/naming.ts +122 -25
  25. package/src/node/node-overrides.ts +77 -0
  26. package/src/node/options.ts +41 -0
  27. package/src/node/path-expression.ts +11 -4
  28. package/src/node/resources.ts +473 -48
  29. package/src/node/sdk-errors.ts +0 -16
  30. package/src/node/tests.ts +152 -93
  31. package/src/node/type-map.ts +40 -18
  32. package/src/node/utils.ts +89 -102
  33. package/src/node/wrappers.ts +0 -20
  34. package/test/node/client.test.ts +106 -1201
  35. package/test/node/enums.test.ts +59 -130
  36. package/test/node/errors.test.ts +2 -3
  37. package/test/node/live-surface.test.ts +240 -0
  38. package/test/node/models.test.ts +396 -765
  39. package/test/node/naming.test.ts +69 -234
  40. package/test/node/resources.test.ts +435 -2025
  41. package/test/node/tests.test.ts +214 -0
  42. package/test/node/type-map.test.ts +49 -54
  43. package/test/node/utils.test.ts +29 -80
  44. package/dist/plugin-CmfzawTp.mjs.map +0 -1
  45. package/test/node/serializers.test.ts +0 -444
@@ -11,7 +11,39 @@ import type {
11
11
  } from '@workos/oagen';
12
12
  import { planOperation, toPascalCase, toCamelCase } from '@workos/oagen';
13
13
  import type { OperationPlan } from '@workos/oagen';
14
- import { mapTypeRef } from './type-map.js';
14
+ import { mapTypeRef, isInlineEnum } from './type-map.js';
15
+ import { liveSurfaceHasFunction, liveSurfaceHasFile, liveSurfaceHasAutogenFile } from './live-surface.js';
16
+
17
+ /**
18
+ * Render the request-body argument for an HTTP call.
19
+ *
20
+ * Three cases:
21
+ * 1. `serialize${T}` is in the live SDK's serializer functions → call it.
22
+ * 2. The serializer file exists on disk but does *not* export `serialize${T}`
23
+ * (e.g. workos-node's `validate-api-key.serializer.ts` ships only the
24
+ * response deserializer). The user owns the file; we cannot add to it,
25
+ * so pass `payload` straight through. `*Options` interfaces in
26
+ * workos-node are already wire-shaped (primitives or pre-snake_cased
27
+ * keys), so this preserves correctness for the workos-node convention.
28
+ * 3. Otherwise the emitter is producing the serializer this run → call.
29
+ */
30
+ function bodyArgExpr(irModelName: string, resolvedName: string, ctx: EmitterContext, paramName = 'payload'): string {
31
+ const ser = `serialize${resolvedName}`;
32
+ if (liveSurfaceHasFunction(ser)) return `${ser}(${paramName})`;
33
+
34
+ const sourceFile = (ctx.apiSurface?.interfaces?.[resolvedName] as { sourceFile?: string } | undefined)?.sourceFile;
35
+ const candidate = sourceFile
36
+ ? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
37
+ : `src/${defaultModelDir(irModelName, ctx)}/serializers/${fileName(irModelName)}.serializer.ts`;
38
+ if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) return paramName;
39
+
40
+ return `${ser}(${paramName})`;
41
+ }
42
+
43
+ function defaultModelDir(irModelName: string, ctx: EmitterContext): string {
44
+ const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
45
+ return resolveDir(modelToService.get(irModelName));
46
+ }
15
47
  import {
16
48
  fieldName,
17
49
  wireFieldName,
@@ -28,6 +60,7 @@ import {
28
60
  isServiceCoveredByExisting,
29
61
  hasMethodsAbsentFromBaseline,
30
62
  uncoveredOperations,
63
+ relativeImport,
31
64
  } from './utils.js';
32
65
  import { assignEnumsToServices } from './enums.js';
33
66
  import { unwrapListModel } from './fixtures.js';
@@ -42,6 +75,7 @@ import {
42
75
  import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
43
76
  import { buildNodePathExpression } from './path-expression.js';
44
77
  import { resolveWrapperParams } from '../shared/wrapper-utils.js';
78
+ import { isNodeOwnedService } from './options.js';
45
79
 
46
80
  /**
47
81
  * Check whether the baseline (hand-written) class has a constructor compatible
@@ -79,6 +113,12 @@ export function resolveResourceClassName(service: Service, ctx: EmitterContext):
79
113
  return irName;
80
114
  }
81
115
 
116
+ export function resolveResourceDir(service: Service, ctx: EmitterContext): string {
117
+ const resolvedName = resolveResourceClassName(service, ctx);
118
+ if (resolvedName === 'WebhooksEndpoints') return 'webhooks';
119
+ return resolveServiceDir(resolvedName);
120
+ }
121
+
82
122
  /** Standard pagination query params handled by PaginationOptions — not imported individually. */
83
123
  const PAGINATION_PARAM_NAMES = new Set(['limit', 'before', 'after', 'order']);
84
124
 
@@ -99,6 +139,76 @@ function paginatedOptionsName(method: string, resolvedServiceName: string): stri
99
139
  return toPascalCase(method) + 'Options';
100
140
  }
101
141
 
142
+ type BaselineMethod = {
143
+ params: Array<{ name: string; type: string; optional?: boolean; passingStyle?: string }>;
144
+ returnType?: string;
145
+ };
146
+
147
+ function baselineMethodFor(service: Service, method: string, ctx: EmitterContext): BaselineMethod | undefined {
148
+ const serviceClass = resolveResourceClassName(service, ctx);
149
+ return ctx.apiSurface?.classes?.[serviceClass]?.methods?.[method]?.[0] as BaselineMethod | undefined;
150
+ }
151
+
152
+ function optionsObjectParam(method: BaselineMethod | undefined): { name: string; type: string } | undefined {
153
+ if (!method || method.params.length !== 1) return undefined;
154
+ const [param] = method.params;
155
+ if (param.name !== 'options') return undefined;
156
+ if (param.passingStyle && param.passingStyle !== 'options_object') return undefined;
157
+ if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return undefined;
158
+ return { name: param.name, type: param.type };
159
+ }
160
+
161
+ function autoPaginatableItemType(returnType: string | undefined): string | undefined {
162
+ return returnType?.match(/\bAutoPaginatable<\s*([A-Za-z_$][\w$]*)/)?.[1];
163
+ }
164
+
165
+ function baselineTypeSourceFile(ctx: EmitterContext, typeName: string): string | undefined {
166
+ const surface = ctx.apiSurface as
167
+ | {
168
+ interfaces?: Record<string, { sourceFile?: string }>;
169
+ typeAliases?: Record<string, { sourceFile?: string }>;
170
+ enums?: Record<string, { sourceFile?: string }>;
171
+ }
172
+ | undefined;
173
+ return (
174
+ surface?.interfaces?.[typeName]?.sourceFile ??
175
+ surface?.typeAliases?.[typeName]?.sourceFile ??
176
+ surface?.enums?.[typeName]?.sourceFile
177
+ );
178
+ }
179
+
180
+ function preferredBaselineTypeName(ctx: EmitterContext, typeName: string | undefined): string | undefined {
181
+ if (!typeName) return undefined;
182
+ const surface = ctx.apiSurface as
183
+ | {
184
+ typeAliases?: Record<string, { value?: string; sourceFile?: string }>;
185
+ interfaces?: Record<string, { sourceFile?: string }>;
186
+ }
187
+ | undefined;
188
+ const sourceFile = baselineTypeSourceFile(ctx, typeName);
189
+ for (const [alias, info] of Object.entries(surface?.typeAliases ?? {})) {
190
+ if (info.value !== typeName) continue;
191
+ if (sourceFile && info.sourceFile !== sourceFile) continue;
192
+ if (liveSurfaceHasFunction(`deserialize${alias}`)) return alias;
193
+ }
194
+ return typeName;
195
+ }
196
+
197
+ function preferredBaselineReturnType(ctx: EmitterContext, returnType: string | undefined): string | undefined {
198
+ const itemType = autoPaginatableItemType(returnType);
199
+ const preferred = preferredBaselineTypeName(ctx, itemType);
200
+ if (!returnType || !itemType || !preferred || preferred === itemType) return returnType;
201
+ return returnType.replace(new RegExp(`\\b${itemType}\\b`, 'g'), preferred);
202
+ }
203
+
204
+ function requestEntityType(bodyExpr: string, requestType: string): string {
205
+ return bodyExpr === 'payload' ? requestType : wireInterfaceName(requestType);
206
+ }
207
+
208
+ function unionEntityType(modelNames: string[], ctx: EmitterContext): string {
209
+ return modelNames.map((name) => wireInterfaceName(resolveInterfaceName(name, ctx))).join(' | ');
210
+ }
211
+
102
212
  /** HTTP methods that require a body argument even when the spec has no request body. */
103
213
  function httpMethodNeedsBody(method: string): boolean {
104
214
  return method === 'post' || method === 'put' || method === 'patch';
@@ -209,7 +319,7 @@ function generatePaginatedOptionsInterfaces(
209
319
  ): GeneratedFile[] {
210
320
  const files: GeneratedFile[] = [];
211
321
  const resolvedName = resolveResourceClassName(service, ctx);
212
- const serviceDir = resolveServiceDir(resolvedName);
322
+ const serviceDir = resolveResourceDir(service, ctx);
213
323
 
214
324
  const plans = service.operations.map((op) => ({
215
325
  op,
@@ -263,12 +373,14 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
263
373
  const topLevelEnumNames = new Set(ctx.spec.enums.map((e) => e.name));
264
374
 
265
375
  for (const service of mergedServices) {
266
- if (isServiceCoveredByExisting(service, ctx)) {
376
+ const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
377
+ if (!isOwnedService && isServiceCoveredByExisting(service, ctx)) {
267
378
  if (!hasMethodsAbsentFromBaseline(service, ctx)) {
268
379
  continue; // Fully covered, no new methods -- skip entirely
269
380
  }
270
381
  // Partially covered -- has new methods that need to be added.
271
382
  const file = generateResourceClass(service, ctx);
383
+ if (!file) continue;
272
384
  delete file.skipIfExists;
273
385
  // Suppress auto-generated header — the file is a merge target
274
386
  // containing hand-written code, not a fully generated file.
@@ -277,7 +389,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
277
389
  continue;
278
390
  }
279
391
 
280
- const ops = uncoveredOperations(service, ctx);
392
+ const ops = isOwnedService ? service.operations : uncoveredOperations(service, ctx);
281
393
  if (ops.length === 0) continue;
282
394
 
283
395
  if (ops.length < service.operations.length) {
@@ -285,6 +397,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
285
397
  // for both covered and uncovered methods. Remove skipIfExists so the
286
398
  // merger adds new methods AND refreshes existing JSDoc.
287
399
  const file = generateResourceClass(service, ctx);
400
+ if (!file) continue;
288
401
  delete file.skipIfExists;
289
402
  // Suppress auto-generated header — the file is a merge target
290
403
  // containing hand-written code, not a fully generated file.
@@ -296,6 +409,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
296
409
  // overwrites — emitter improvements (serializer dispatch, JSDoc, etc.)
297
410
  // must propagate without manual intervention.
298
411
  const file = generateResourceClass(service, ctx);
412
+ if (!file) continue;
299
413
  delete file.skipIfExists;
300
414
  files.push(file);
301
415
  }
@@ -306,20 +420,22 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
306
420
  // stable. Placing them under `interfaces/` lets the per-service barrel
307
421
  // pick them up automatically.
308
422
  for (const service of mergedServices) {
309
- if (isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx)) continue;
423
+ const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
424
+ if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
425
+ continue;
310
426
  files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
311
427
  }
312
428
 
313
429
  return files;
314
430
  }
315
431
 
316
- function generateResourceClass(service: Service, ctx: EmitterContext): GeneratedFile {
432
+ function generateResourceClass(service: Service, ctx: EmitterContext): GeneratedFile | null {
317
433
  const resolvedName = resolveResourceClassName(service, ctx);
318
- const serviceDir = resolveServiceDir(resolvedName);
434
+ const serviceDir = resolveResourceDir(service, ctx);
319
435
  const serviceClass = resolvedName;
320
436
  const resourcePath = `src/${serviceDir}/${fileName(resolvedName)}.ts`;
321
437
 
322
- const plans = service.operations.map((op) => ({
438
+ let plans = service.operations.map((op) => ({
323
439
  op,
324
440
  plan: planOperation(op),
325
441
  method: resolveMethodName(op, service, ctx),
@@ -362,28 +478,60 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
362
478
  }
363
479
  }
364
480
 
481
+ // Filter out operations whose method name already exists in the baseline
482
+ // class. The user's `@oagen-ignore-start`/`@oagen-ignore-end` blocks in
483
+ // the existing file preserve those method bodies; emitting them again
484
+ // would duplicate the symbol after the engine appends the preserved block
485
+ // (engine writeFiles → overwriteWithPreservedRegions appends, doesn't
486
+ // replace, when the block is method-level not class-level).
487
+ //
488
+ // Direct check against `apiSurface.classes[serviceClass].methods` rather
489
+ // than `uncoveredOperations`, which routes through `overlayLookup` that
490
+ // isn't always populated.
491
+ //
492
+ // Done after dedup + sort so the rendered methods keep their stable names.
493
+ // Imports below filter through `plans`, so they automatically narrow to
494
+ // the kept operations only — no orphan imports.
495
+ const baselineMethodNames = new Set(Object.keys(ctx.apiSurface?.classes?.[serviceClass]?.methods ?? {}));
496
+ const planCountBeforeFilter = plans.length;
497
+ if (!isNodeOwnedService(ctx, service.name, serviceClass) && baselineMethodNames.size > 0) {
498
+ plans = plans.filter((p) => !baselineMethodNames.has(p.method));
499
+ }
500
+ const filteredOut = planCountBeforeFilter - plans.length;
501
+
502
+ // Skip emitting the file altogether when every operation already exists in
503
+ // baseline AND the existing class is preserved via `@oagen-ignore-start`
504
+ // blocks. The autogen-overwrite path would otherwise write an empty class
505
+ // shell that the engine's region preservation would have to reconstruct.
506
+ if (plans.length === 0 && filteredOut > 0) {
507
+ return null;
508
+ }
509
+
365
510
  const hasPaginated = plans.some((p) => p.plan.isPaginated);
511
+ const needsPaginationOptionsImport = plans.some(
512
+ (p) =>
513
+ p.plan.isPaginated &&
514
+ (!optionsObjectParam(baselineMethodFor(service, p.method, ctx)) ||
515
+ /\bPaginationOptions\b/.test(
516
+ preferredBaselineReturnType(ctx, baselineMethodFor(service, p.method, ctx)?.returnType) ?? '',
517
+ )),
518
+ );
366
519
  const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
367
520
 
368
- // When merging into an existing class, the merger keeps baseline method
369
- // bodies but may add imports from the generated code. To avoid orphaned
370
- // imports for types used only by baseline methods (whose bodies are kept
371
- // intact), skip model collection for methods that already exist.
372
- const baselineMethodSet = new Set<string>();
373
- const baselineClass = ctx.apiSurface?.classes?.[serviceClass];
374
- if (baselineClass?.methods) {
375
- for (const name of Object.keys(baselineClass.methods)) {
376
- baselineMethodSet.add(name);
377
- }
378
- }
379
-
380
521
  // Collect models for imports — only include models that are actually used
381
522
  // in method signatures (not all union variants from the spec)
382
523
  const responseModels = new Set<string>();
383
524
  const requestModels = new Set<string>();
525
+ const requestModelsForSignature = new Set<string>();
384
526
  const paramEnums = new Set<string>();
385
527
  const paramModels = new Set<string>();
386
- for (const { op, plan, method } of plans) {
528
+ const optionObjectTypes = new Set<string>();
529
+ const baselineResponseTypes = new Set<string>();
530
+ for (const { op, plan } of plans) {
531
+ const baselineMethod = baselineMethodFor(service, resolveMethodName(op, service, ctx), ctx);
532
+ const existingOptions = optionsObjectParam(baselineMethod);
533
+ if (existingOptions) optionObjectTypes.add(existingOptions.type);
534
+
387
535
  // Always collect param type refs for enums — inline options interfaces
388
536
  // are generated for all methods (including baseline ones), so their
389
537
  // type dependencies must always be imported.
@@ -394,12 +542,20 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
394
542
  collectParamTypeRefs(param.type, paramEnums, paramModels);
395
543
  }
396
544
 
397
- // Skip response/request model imports for methods that already exist in
398
- // the baseline class. The merger keeps baseline method bodies, so their
399
- // imports are already present in the existing file.
400
- if (baselineMethodSet.has(method)) continue;
401
-
402
- if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
545
+ // Always collect imports for every rendered method. Earlier versions
546
+ // skipped baseline methods on the assumption the AST merger would keep
547
+ // their existing imports but the autogen-aware writer in
548
+ // `node/index.ts` overwrites previously-generated files in full so spec
549
+ // renames propagate. With overwrite, missing imports become real
550
+ // compile errors. Redundant imports are harmless (eslint --fix prunes
551
+ // them post-generation via `formatCommand`).
552
+
553
+ const baselinePaginatedItemType = existingOptions
554
+ ? preferredBaselineTypeName(ctx, autoPaginatableItemType(baselineMethod?.returnType))
555
+ : undefined;
556
+ if (plan.isPaginated && baselinePaginatedItemType) {
557
+ baselineResponseTypes.add(baselinePaginatedItemType);
558
+ } else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
403
559
  // For paginated operations, import the item type (e.g., Connection)
404
560
  // rather than the list wrapper type (e.g., ConnectionList).
405
561
  // fetchAndDeserialize handles the list envelope internally.
@@ -420,18 +576,21 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
420
576
  const bodyInfo = extractRequestBodyType(op, ctx);
421
577
  if (bodyInfo?.kind === 'model') {
422
578
  requestModels.add(bodyInfo.name);
579
+ if (!existingOptions) requestModelsForSignature.add(bodyInfo.name);
423
580
  } else if (bodyInfo?.kind === 'union') {
424
581
  if (bodyInfo.discriminator) {
425
582
  // Discriminated union: import variant models with serializers so we can
426
583
  // dispatch to the correct serializer at runtime based on the discriminator.
427
584
  for (const name of bodyInfo.modelNames) {
428
585
  requestModels.add(name);
586
+ if (!existingOptions) requestModelsForSignature.add(name);
429
587
  }
430
588
  } else {
431
589
  // Non-discriminated union: import variant models with serializers so we
432
590
  // can dispatch to the correct serializer at runtime via field guards.
433
591
  for (const name of bodyInfo.modelNames) {
434
592
  requestModels.add(name);
593
+ if (!existingOptions) requestModelsForSignature.add(name);
435
594
  }
436
595
  }
437
596
  }
@@ -443,8 +602,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
443
602
  // `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
444
603
  // reference to a type it never imported.
445
604
  const resolvedLookup = buildResolvedLookup(ctx);
446
- for (const { op, method } of plans) {
447
- if (baselineMethodSet.has(method)) continue;
605
+ for (const { op } of plans) {
448
606
  const resolved = lookupResolved(op, resolvedLookup);
449
607
  if (resolved) {
450
608
  for (const name of collectWrapperResponseModels(resolved)) {
@@ -458,14 +616,16 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
458
616
  }
459
617
  }
460
618
 
461
- const allModels = new Set([...responseModels, ...requestModels, ...paramModels]);
619
+ const allModels = new Set([...responseModels, ...requestModelsForSignature, ...paramModels]);
462
620
 
463
621
  const lines: string[] = [];
464
622
 
465
623
  // Imports
466
624
  lines.push("import type { WorkOS } from '../workos';");
467
625
  if (hasPaginated) {
468
- lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
626
+ if (needsPaginationOptionsImport) {
627
+ lines.push("import type { PaginationOptions } from '../common/interfaces/pagination-options.interface';");
628
+ }
469
629
  lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
470
630
  lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
471
631
  }
@@ -490,6 +650,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
490
650
  lines.push("import type { PostOptions } from '../common/interfaces/post-options.interface';");
491
651
  }
492
652
 
653
+ const importedTypeNames = new Set<string>();
654
+ for (const optionType of optionObjectTypes) {
655
+ if (importedTypeNames.has(optionType)) continue;
656
+ importedTypeNames.add(optionType);
657
+ lines.push(`import type { ${optionType} } from './interfaces/${fileName(optionType)}.interface';`);
658
+ }
659
+
493
660
  // Compute model-to-service mapping for correct cross-service import paths
494
661
  const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
495
662
 
@@ -499,9 +666,15 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
499
666
  for (const name of responseModels) {
500
667
  usedWireTypes.add(resolveInterfaceName(name, ctx));
501
668
  }
669
+ for (const name of requestModels) {
670
+ const resolved = resolveInterfaceName(name, ctx);
671
+ const bodyExpr = bodyArgExpr(name, resolved, ctx);
672
+ if (requestEntityType(bodyExpr, resolved) !== resolved) {
673
+ usedWireTypes.add(resolved);
674
+ }
675
+ }
502
676
 
503
677
  // Track imported resolved names to prevent duplicate type name collisions
504
- const importedTypeNames = new Set<string>();
505
678
  for (const name of allModels) {
506
679
  const resolved = resolveInterfaceName(name, ctx);
507
680
  if (importedTypeNames.has(resolved)) continue; // Skip duplicate resolved names
@@ -519,6 +692,33 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
519
692
  }
520
693
  }
521
694
 
695
+ for (const name of requestModels) {
696
+ if (allModels.has(name)) continue;
697
+ const resolved = resolveInterfaceName(name, ctx);
698
+ if (!usedWireTypes.has(resolved)) continue;
699
+ const wireName = wireInterfaceName(resolved);
700
+ if (importedTypeNames.has(wireName)) continue;
701
+ const modelDir = modelToService.get(name);
702
+ const modelServiceDir = resolveDir(modelDir);
703
+ const relPath =
704
+ modelServiceDir === serviceDir
705
+ ? `./interfaces/${fileName(name)}.interface`
706
+ : `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
707
+ lines.push(`import type { ${wireName} } from '${relPath}';`);
708
+ importedTypeNames.add(wireName);
709
+ }
710
+
711
+ for (const name of baselineResponseTypes) {
712
+ if (importedTypeNames.has(name)) continue;
713
+ const wireName = wireInterfaceName(name);
714
+ const sourceFile = baselineTypeSourceFile(ctx, name) ?? baselineTypeSourceFile(ctx, wireName);
715
+ if (!sourceFile) continue;
716
+ const importNames = wireName === name ? name : `${name}, ${wireName}`;
717
+ lines.push(`import type { ${importNames} } from '${relativeImport(resourcePath, sourceFile)}';`);
718
+ importedTypeNames.add(name);
719
+ importedTypeNames.add(wireName);
720
+ }
721
+
522
722
  // Collect serializer imports by module path so we can merge deserialize and
523
723
  // serialize imports from the same module into a single import statement.
524
724
  const serializerImportsByPath = new Map<string, string[]>();
@@ -538,12 +738,38 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
538
738
  existing.push(`deserialize${resolved}`);
539
739
  serializerImportsByPath.set(relPath, existing);
540
740
  }
741
+ for (const name of baselineResponseTypes) {
742
+ if (importedDeserializers.has(name)) continue;
743
+ importedDeserializers.add(name);
744
+ const sourceFile = baselineTypeSourceFile(ctx, name) ?? baselineTypeSourceFile(ctx, wireInterfaceName(name));
745
+ if (!sourceFile) continue;
746
+ const serializerPath = sourceFile
747
+ .replace('/interfaces/', '/serializers/')
748
+ .replace(/\.interface\.ts$/, '.serializer.ts');
749
+ const relPath = relativeImport(resourcePath, serializerPath);
750
+ const existing = serializerImportsByPath.get(relPath) ?? [];
751
+ existing.push(`deserialize${name}`);
752
+ serializerImportsByPath.set(relPath, existing);
753
+ }
541
754
 
542
755
  const importedSerializers = new Set<string>();
543
756
  for (const name of requestModels) {
544
757
  const resolved = resolveInterfaceName(name, ctx);
545
758
  if (importedSerializers.has(resolved)) continue;
546
759
  importedSerializers.add(resolved);
760
+
761
+ // If `bodyArgExpr` will fall through to passing `payload` directly
762
+ // (because the live SDK has the serializer file but no `serialize${T}`
763
+ // function), don't generate an import for a function that doesn't exist.
764
+ const ser = `serialize${resolved}`;
765
+ if (!liveSurfaceHasFunction(ser)) {
766
+ const sourceFile = (ctx.apiSurface?.interfaces?.[resolved] as { sourceFile?: string } | undefined)?.sourceFile;
767
+ const candidate = sourceFile
768
+ ? sourceFile.replace('/interfaces/', '/serializers/').replace('.interface.ts', '.serializer.ts')
769
+ : `src/${resolveDir(modelToService.get(name))}/serializers/${fileName(name)}.serializer.ts`;
770
+ if (liveSurfaceHasFile(candidate) && !liveSurfaceHasAutogenFile(candidate)) continue;
771
+ }
772
+
547
773
  const modelDir = modelToService.get(name);
548
774
  const modelServiceDir = resolveDir(modelDir);
549
775
  const relPath =
@@ -551,7 +777,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
551
777
  ? `./serializers/${fileName(name)}.serializer`
552
778
  : `../${modelServiceDir}/serializers/${fileName(name)}.serializer`;
553
779
  const existing = serializerImportsByPath.get(relPath) ?? [];
554
- existing.push(`serialize${resolved}`);
780
+ existing.push(ser);
555
781
  serializerImportsByPath.set(relPath, existing);
556
782
  }
557
783
 
@@ -568,10 +794,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
568
794
  // Only import enums that actually exist in the spec's global enums list —
569
795
  // inline string unions may have kind 'enum' but no corresponding file.
570
796
  if (paramEnums.size > 0) {
571
- const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services);
797
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services, ctx.spec.models, ctx);
572
798
  for (const name of paramEnums) {
573
799
  if (allModels.has(name)) continue; // Already imported as a model
574
800
  if (!specEnumNames.has(name)) continue; // No file generated for this enum
801
+ if (isInlineEnum(name)) continue; // Inlined at usage sites — no import needed
575
802
  const enumDir = enumToService.get(name);
576
803
  const enumServiceDir = resolveDir(enumDir);
577
804
  const relPath =
@@ -700,7 +927,8 @@ function renderMethod(
700
927
  // Prefer the overlay (existing method signature) if available;
701
928
  // otherwise compute from what the render path will actually include.
702
929
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
703
- const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey);
930
+ const baselineClassMethod = baselineMethodFor(service, method, ctx);
931
+ const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey) ?? baselineClassMethod;
704
932
  let validParamNames: Set<string> | null = null;
705
933
  if (overlayMethod) {
706
934
  validParamNames = new Set(overlayMethod.params.map((p) => p.name));
@@ -886,8 +1114,11 @@ function renderMethod(
886
1114
  // When an overlay method exists, prefer its return type so the JSDoc
887
1115
  // matches the actual TypeScript signature (the overlay may use a
888
1116
  // different model name than the OpenAPI schema).
889
- if (overlayMethod?.returnType) {
890
- docParts.push(`@returns {${overlayMethod.returnType}}`);
1117
+ const documentedReturnType =
1118
+ preferredBaselineReturnType(ctx, baselineClassMethod?.returnType) ??
1119
+ preferredBaselineReturnType(ctx, overlayMethod?.returnType);
1120
+ if (documentedReturnType) {
1121
+ docParts.push(`@returns {${documentedReturnType}}`);
891
1122
  } else if (plan.isPaginated && op.pagination?.itemType.kind === 'model') {
892
1123
  // Unwrap list wrapper models to match the actual return type — the method returns
893
1124
  // AutoPaginatable<ItemType>, not the list wrapper.
@@ -934,6 +1165,10 @@ function renderMethod(
934
1165
  }
935
1166
  }
936
1167
 
1168
+ if (renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelMap, specEnumNames, baselineClassMethod)) {
1169
+ return lines;
1170
+ }
1171
+
937
1172
  const preDecisionCount = lines.length;
938
1173
 
939
1174
  if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
@@ -992,6 +1227,184 @@ function renderMethod(
992
1227
  return lines;
993
1228
  }
994
1229
 
1230
+ function renderOptionsObjectMethod(
1231
+ lines: string[],
1232
+ op: Operation,
1233
+ plan: OperationPlan,
1234
+ method: string,
1235
+ service: Service,
1236
+ ctx: EmitterContext,
1237
+ modelMap: Map<string, Model>,
1238
+ specEnumNames: Set<string> | undefined,
1239
+ baselineMethod: BaselineMethod | undefined,
1240
+ ): boolean {
1241
+ const optionParam = optionsObjectParam(baselineMethod);
1242
+ if (!optionParam) return false;
1243
+
1244
+ const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
1245
+ const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
1246
+ const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
1247
+
1248
+ if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
1249
+ let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
1250
+ if (itemRawName) {
1251
+ const pModel = modelMap.get(itemRawName);
1252
+ if (pModel) {
1253
+ const unwrapped = unwrapListModel(pModel, modelMap);
1254
+ if (unwrapped) itemRawName = unwrapped.name;
1255
+ }
1256
+ }
1257
+ const itemType =
1258
+ preferredBaselineTypeName(ctx, autoPaginatableItemType(baselineMethod?.returnType)) ??
1259
+ (itemRawName ? resolveInterfaceName(itemRawName, ctx) : responseModel);
1260
+ if (!itemType) return false;
1261
+ const wireType = wireInterfaceName(itemType);
1262
+ const returnType =
1263
+ preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
1264
+ lines.push(` async ${method}(options: ${optionParam.type}): ${returnType} {`);
1265
+ renderOptionsObjectDestructure(lines, pathBindings, 'paginationOptions');
1266
+ lines.push(` return new AutoPaginatable(`);
1267
+ lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
1268
+ lines.push(` this.workos,`);
1269
+ lines.push(` ${pathStr},`);
1270
+ lines.push(` deserialize${itemType},`);
1271
+ lines.push(` paginationOptions,`);
1272
+ lines.push(` ),`);
1273
+ lines.push(` (params) =>`);
1274
+ lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
1275
+ lines.push(` this.workos,`);
1276
+ lines.push(` ${pathStr},`);
1277
+ lines.push(` deserialize${itemType},`);
1278
+ lines.push(` params,`);
1279
+ lines.push(` ),`);
1280
+ lines.push(` paginationOptions,`);
1281
+ lines.push(` );`);
1282
+ lines.push(' }');
1283
+ return true;
1284
+ }
1285
+
1286
+ if (plan.isDelete && !plan.hasBody) {
1287
+ lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1288
+ renderOptionsObjectDestructure(lines, pathBindings);
1289
+ lines.push(` await this.workos.delete(${pathStr});`);
1290
+ lines.push(' }');
1291
+ return true;
1292
+ }
1293
+
1294
+ if (plan.hasBody) {
1295
+ const bodyInfo = extractRequestBodyType(op, ctx);
1296
+ let requestType: string;
1297
+ let bodyExpr: string;
1298
+ let entityType: string;
1299
+ if (bodyInfo?.kind === 'model') {
1300
+ requestType = resolveInterfaceName(bodyInfo.name, ctx);
1301
+ bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx, 'payload');
1302
+ entityType = requestEntityType(bodyExpr, requestType);
1303
+ } else if (bodyInfo?.kind === 'union') {
1304
+ requestType = bodyInfo.typeStr;
1305
+ bodyExpr = bodyInfo.discriminator
1306
+ ? renderUnionBodySerializer(bodyInfo.discriminator, ctx)
1307
+ : renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
1308
+ entityType = unionEntityType(bodyInfo.modelNames, ctx);
1309
+ } else {
1310
+ requestType = 'Record<string, unknown>';
1311
+ bodyExpr = 'payload';
1312
+ entityType = requestType;
1313
+ }
1314
+
1315
+ if (plan.isDelete) {
1316
+ lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1317
+ renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1318
+ lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
1319
+ lines.push(' }');
1320
+ return true;
1321
+ }
1322
+
1323
+ if (!responseModel) {
1324
+ lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1325
+ renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1326
+ lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
1327
+ lines.push(' }');
1328
+ return true;
1329
+ }
1330
+
1331
+ const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1332
+ const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1333
+ const returnExpr = plan.isArrayResponse
1334
+ ? `data.map(deserialize${responseModel})`
1335
+ : `deserialize${responseModel}(data)`;
1336
+
1337
+ lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1338
+ renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1339
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1340
+ lines.push(` ${pathStr},`);
1341
+ lines.push(` ${bodyExpr},`);
1342
+ lines.push(' );');
1343
+ lines.push(` return ${returnExpr};`);
1344
+ lines.push(' }');
1345
+ return true;
1346
+ }
1347
+
1348
+ if (responseModel) {
1349
+ const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1350
+ const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1351
+ const returnExpr = plan.isArrayResponse
1352
+ ? `data.map(deserialize${responseModel})`
1353
+ : `deserialize${responseModel}(data)`;
1354
+
1355
+ lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1356
+ renderOptionsObjectDestructure(lines, pathBindings);
1357
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
1358
+ lines.push(` return ${returnExpr};`);
1359
+ lines.push(' }');
1360
+ return true;
1361
+ }
1362
+
1363
+ return false;
1364
+ }
1365
+
1366
+ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[], restName?: string): void {
1367
+ if (pathBindings.length > 0 && restName) {
1368
+ lines.push(` const { ${pathBindings.join(', ')}, ...${restName} } = options;`);
1369
+ } else if (pathBindings.length > 0) {
1370
+ lines.push(` const { ${pathBindings.join(', ')} } = options;`);
1371
+ } else if (restName) {
1372
+ lines.push(` const ${restName} = options;`);
1373
+ }
1374
+ }
1375
+
1376
+ function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
1377
+ // Return resolved SDK field names directly — the URL template uses these
1378
+ // names too (via the param-name map threaded into buildNodePathExpression),
1379
+ // so the destructure no longer needs `optionField: localName` renames.
1380
+ return op.pathParams.map((param) => resolveOptionsObjectField(fieldName(param.name), optionType, ctx));
1381
+ }
1382
+
1383
+ /**
1384
+ * Map spec path-param names (e.g. `omId`) to the SDK field name exposed on
1385
+ * the options interface (e.g. `organizationMembershipId`). Empty when every
1386
+ * path param's spec name already matches the SDK field. Consumed by
1387
+ * `buildNodePathExpression` so the URL template binds to the same identifier
1388
+ * the destructure does.
1389
+ */
1390
+ function buildOptionsObjectPathParamMap(op: Operation, optionType: string, ctx: EmitterContext): Map<string, string> {
1391
+ const map = new Map<string, string>();
1392
+ for (const param of op.pathParams) {
1393
+ const localName = fieldName(param.name);
1394
+ const sdkField = resolveOptionsObjectField(localName, optionType, ctx);
1395
+ if (sdkField !== localName) map.set(param.name, sdkField);
1396
+ }
1397
+ return map;
1398
+ }
1399
+
1400
+ function resolveOptionsObjectField(localName: string, optionType: string, ctx: EmitterContext): string {
1401
+ const fields = ctx.apiSurface?.interfaces?.[optionType]?.fields;
1402
+ if (!fields) return localName;
1403
+ if (fields[localName]) return localName;
1404
+ if (localName === 'omId' && fields.organizationMembershipId) return 'organizationMembershipId';
1405
+ return localName;
1406
+ }
1407
+
995
1408
  function renderPaginatedMethod(
996
1409
  lines: string[],
997
1410
  op: Operation,
@@ -1062,9 +1475,11 @@ function renderDeleteWithBodyMethod(
1062
1475
  const bodyInfo = extractRequestBodyType(op, ctx);
1063
1476
  let requestType: string;
1064
1477
  let bodyExpr: string;
1478
+ let entityType: string;
1065
1479
  if (bodyInfo?.kind === 'model') {
1066
1480
  requestType = resolveInterfaceName(bodyInfo.name, ctx);
1067
- bodyExpr = `serialize${requestType}(payload)`;
1481
+ bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
1482
+ entityType = requestEntityType(bodyExpr, requestType);
1068
1483
  } else if (bodyInfo?.kind === 'union') {
1069
1484
  requestType = bodyInfo.typeStr;
1070
1485
  if (bodyInfo.discriminator) {
@@ -1072,9 +1487,11 @@ function renderDeleteWithBodyMethod(
1072
1487
  } else {
1073
1488
  bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
1074
1489
  }
1490
+ entityType = unionEntityType(bodyInfo.modelNames, ctx);
1075
1491
  } else {
1076
1492
  requestType = 'Record<string, unknown>';
1077
1493
  bodyExpr = 'payload';
1494
+ entityType = requestType;
1078
1495
  }
1079
1496
 
1080
1497
  const paramParts: string[] = [];
@@ -1086,7 +1503,7 @@ function renderDeleteWithBodyMethod(
1086
1503
  paramParts.push(`payload: ${requestType}`);
1087
1504
 
1088
1505
  lines.push(` async ${method}(${paramParts.join(', ')}): Promise<void> {`);
1089
- lines.push(` await this.workos.deleteWithBody(${pathStr}, ${bodyExpr});`);
1506
+ lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
1090
1507
  lines.push(' }');
1091
1508
  }
1092
1509
 
@@ -1103,9 +1520,11 @@ function renderBodyMethod(
1103
1520
  const bodyInfo = extractRequestBodyType(op, ctx);
1104
1521
  let requestType: string;
1105
1522
  let bodyExpr: string;
1523
+ let entityType: string;
1106
1524
  if (bodyInfo?.kind === 'model') {
1107
1525
  requestType = resolveInterfaceName(bodyInfo.name, ctx);
1108
- bodyExpr = `serialize${requestType}(payload)`;
1526
+ bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
1527
+ entityType = requestEntityType(bodyExpr, requestType);
1109
1528
  } else if (bodyInfo?.kind === 'union') {
1110
1529
  requestType = bodyInfo.typeStr;
1111
1530
  if (bodyInfo.discriminator) {
@@ -1113,9 +1532,11 @@ function renderBodyMethod(
1113
1532
  } else {
1114
1533
  bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
1115
1534
  }
1535
+ entityType = unionEntityType(bodyInfo.modelNames, ctx);
1116
1536
  } else {
1117
1537
  requestType = 'Record<string, unknown>';
1118
1538
  bodyExpr = 'payload';
1539
+ entityType = requestType;
1119
1540
  }
1120
1541
 
1121
1542
  const paramParts: string[] = [];
@@ -1149,13 +1570,13 @@ function renderBodyMethod(
1149
1570
  lines.push(` async ${method}(${paramsStr}): Promise<${returnType}> {`);
1150
1571
  if (plan.isIdempotentPost) {
1151
1572
  if (hasCustomEncoding) {
1152
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
1573
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1153
1574
  lines.push(` ${pathStr},`);
1154
1575
  lines.push(` ${bodyExpr},`);
1155
1576
  lines.push(` { ...requestOptions${encodingOption} },`);
1156
1577
  lines.push(' );');
1157
1578
  } else {
1158
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
1579
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1159
1580
  lines.push(` ${pathStr},`);
1160
1581
  lines.push(` ${bodyExpr},`);
1161
1582
  lines.push(' requestOptions,');
@@ -1163,13 +1584,13 @@ function renderBodyMethod(
1163
1584
  }
1164
1585
  } else {
1165
1586
  if (hasCustomEncoding) {
1166
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
1587
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1167
1588
  lines.push(` ${pathStr},`);
1168
1589
  lines.push(` ${bodyExpr},`);
1169
1590
  lines.push(` { ${encodingOption.slice(2)} },`);
1170
1591
  lines.push(' );');
1171
1592
  } else {
1172
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(`);
1593
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1173
1594
  lines.push(` ${pathStr},`);
1174
1595
  lines.push(` ${bodyExpr},`);
1175
1596
  lines.push(' );');
@@ -1316,12 +1737,14 @@ function renderVoidMethod(
1316
1737
 
1317
1738
  let bodyParam = '';
1318
1739
  let bodyExpr = 'payload';
1740
+ let entityType = 'Record<string, unknown>';
1319
1741
  if (plan.hasBody) {
1320
1742
  const bodyInfo = extractRequestBodyType(op, ctx);
1321
1743
  if (bodyInfo?.kind === 'model') {
1322
1744
  const requestType = resolveInterfaceName(bodyInfo.name, ctx);
1323
1745
  bodyParam = `payload: ${requestType}`;
1324
- bodyExpr = `serialize${requestType}(payload)`;
1746
+ bodyExpr = bodyArgExpr(bodyInfo.name, requestType, ctx);
1747
+ entityType = requestEntityType(bodyExpr, requestType);
1325
1748
  } else if (bodyInfo?.kind === 'union') {
1326
1749
  bodyParam = `payload: ${bodyInfo.typeStr}`;
1327
1750
  if (bodyInfo.discriminator) {
@@ -1329,9 +1752,11 @@ function renderVoidMethod(
1329
1752
  } else {
1330
1753
  bodyExpr = renderNonDiscriminatedUnionBodySerializer(bodyInfo.modelNames, ctx);
1331
1754
  }
1755
+ entityType = unionEntityType(bodyInfo.modelNames, ctx);
1332
1756
  } else {
1333
1757
  bodyParam = 'payload: Record<string, unknown>';
1334
1758
  bodyExpr = 'payload';
1759
+ entityType = 'Record<string, unknown>';
1335
1760
  }
1336
1761
  }
1337
1762
 
@@ -1343,7 +1768,7 @@ function renderVoidMethod(
1343
1768
 
1344
1769
  lines.push(` async ${method}(${allParams}): Promise<void> {`);
1345
1770
  if (plan.hasBody) {
1346
- lines.push(` await this.workos.${op.httpMethod}(${pathStr}, ${bodyExpr});`);
1771
+ lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
1347
1772
  } else if (hasQuery) {
1348
1773
  if (hasInjected) {
1349
1774
  // Build query object with visible params, defaults, and inferred fields
@@ -1417,8 +1842,8 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
1417
1842
  return `options ? { ${parts.join(', ')} } : undefined`;
1418
1843
  }
1419
1844
 
1420
- function buildPathStr(op: Operation): string {
1421
- return buildNodePathExpression(op.path);
1845
+ function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
1846
+ return buildNodePathExpression(op.path, paramNameMap);
1422
1847
  }
1423
1848
 
1424
1849
  function buildPathParams(op: Operation, specEnumNames?: Set<string>): string {