@workos/oagen-emitters 0.14.4 → 0.15.1

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 (43) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.d.mts.map +1 -1
  4. package/dist/index.mjs +1 -1
  5. package/dist/{plugin-BGVaMGqe.mjs → plugin-C2Hp2Vs2.mjs} +1039 -274
  6. package/dist/plugin-C2Hp2Vs2.mjs.map +1 -0
  7. package/dist/plugin.mjs +1 -1
  8. package/package.json +7 -7
  9. package/renovate.json +1 -61
  10. package/src/go/client.ts +1 -1
  11. package/src/go/enums.ts +77 -0
  12. package/src/kotlin/enums.ts +11 -4
  13. package/src/node/client.ts +158 -2
  14. package/src/node/discriminated-models.ts +68 -24
  15. package/src/node/field-plan.ts +64 -8
  16. package/src/node/index.ts +59 -3
  17. package/src/node/models.ts +73 -30
  18. package/src/node/naming.ts +14 -1
  19. package/src/node/node-overrides.ts +4 -37
  20. package/src/node/options.ts +29 -1
  21. package/src/node/resources.ts +553 -89
  22. package/src/node/tests.ts +108 -7
  23. package/src/php/fixtures.ts +4 -1
  24. package/src/php/models.ts +3 -1
  25. package/src/php/resources.ts +40 -11
  26. package/src/php/tests.ts +22 -12
  27. package/src/python/client.ts +0 -8
  28. package/src/python/enums.ts +41 -15
  29. package/src/python/fixtures.ts +23 -7
  30. package/src/python/models.ts +26 -5
  31. package/src/python/resources.ts +71 -3
  32. package/src/python/tests.ts +70 -12
  33. package/src/python/wrappers.ts +25 -4
  34. package/src/ruby/client.ts +0 -1
  35. package/src/rust/resources.ts +10 -7
  36. package/src/shared/non-spec-services.ts +0 -5
  37. package/test/go/enums.test.ts +24 -0
  38. package/test/node/resources.test.ts +11 -1
  39. package/test/node/tests.test.ts +3 -3
  40. package/test/php/client.test.ts +0 -1
  41. package/test/php/resources.test.ts +50 -0
  42. package/test/rust/resources.test.ts +9 -0
  43. package/dist/plugin-BGVaMGqe.mjs.map +0 -1
@@ -1,5 +1,7 @@
1
1
  // @oagen-ignore: Operation.async — all TypeScript SDK methods are async by nature
2
2
 
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import type {
4
6
  Service,
5
7
  Operation,
@@ -12,7 +14,12 @@ import type {
12
14
  import { planOperation, toPascalCase, toCamelCase } from '@workos/oagen';
13
15
  import type { OperationPlan } from '@workos/oagen';
14
16
  import { mapTypeRef, isInlineEnum } from './type-map.js';
15
- import { liveSurfaceHasFunction, liveSurfaceHasFile, liveSurfaceHasAutogenFile } from './live-surface.js';
17
+ import {
18
+ liveSurfaceHasFunction,
19
+ liveSurfaceHasFile,
20
+ liveSurfaceHasAutogenFile,
21
+ liveSurfaceInterfacePath,
22
+ } from './live-surface.js';
16
23
 
17
24
  /**
18
25
  * Render the request-body argument for an HTTP call.
@@ -77,7 +84,7 @@ import {
77
84
  import { generateWrapperMethods, collectWrapperResponseModels } from './wrappers.js';
78
85
  import { buildNodePathExpression } from './path-expression.js';
79
86
  import { resolveWrapperParams } from '../shared/wrapper-utils.js';
80
- import { isNodeOwnedService } from './options.js';
87
+ import { isNodeOwnedService, nodeOptions } from './options.js';
81
88
 
82
89
  /**
83
90
  * Check whether the baseline (hand-written) class has a constructor compatible
@@ -156,22 +163,157 @@ type BaselineMethod = {
156
163
  returnType?: string;
157
164
  };
158
165
 
166
+ type OptionsObjectParam = {
167
+ name: 'options';
168
+ type: string;
169
+ optional: boolean;
170
+ generated: boolean;
171
+ };
172
+
173
+ type GeneratedOptionInterfaceExport = {
174
+ stem: string;
175
+ typeName: string;
176
+ };
177
+
178
+ function recordGeneratedOptionInterface(ctx: EmitterContext, serviceDir: string, stem: string, typeName: string): void {
179
+ const key = '_nodeGeneratedOptionInterfaceExports';
180
+ const registry = ((ctx as any)[key] ??= new Map<string, GeneratedOptionInterfaceExport[]>()) as Map<
181
+ string,
182
+ GeneratedOptionInterfaceExport[]
183
+ >;
184
+ const exports = registry.get(serviceDir) ?? [];
185
+ if (!exports.some((entry) => entry.stem === stem && entry.typeName === typeName)) {
186
+ exports.push({ stem, typeName });
187
+ }
188
+ registry.set(serviceDir, exports);
189
+ }
190
+
191
+ function existingInterfaceBarrelExports(ctx: EmitterContext, serviceDir: string, stem: string): boolean {
192
+ const root = ctx.outputDir ?? ctx.targetDir;
193
+ if (!root) return false;
194
+ const barrelPath = path.join(root, 'src', serviceDir, 'interfaces', 'index.ts');
195
+ let content: string;
196
+ try {
197
+ content = fs.readFileSync(barrelPath, 'utf8');
198
+ } catch {
199
+ return false;
200
+ }
201
+ const escapedStem = stem.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
202
+ return new RegExp(`export\\s+(?:type\\s+)?(?:\\*|\\{[^}]+\\})\\s+from\\s+['"]\\./${escapedStem}['"]`).test(content);
203
+ }
204
+
205
+ function operationOverrideFor(ctx: EmitterContext, op: Operation) {
206
+ return nodeOptions(ctx).operationOverrides?.[`${op.httpMethod.toUpperCase()} ${op.path}`];
207
+ }
208
+
159
209
  function baselineMethodFor(service: Service, method: string, ctx: EmitterContext): BaselineMethod | undefined {
160
210
  const serviceClass = resolveResourceClassName(service, ctx);
161
211
  return ctx.apiSurface?.classes?.[serviceClass]?.methods?.[method]?.[0] as BaselineMethod | undefined;
162
212
  }
163
213
 
164
- function optionsObjectParam(method: BaselineMethod | undefined): { name: string; type: string } | undefined {
214
+ function ignoredResourceMethodNames(ctx: EmitterContext, resourcePath: string): Set<string> {
215
+ const root = ctx.outputDir ?? ctx.targetDir;
216
+ if (!root) return new Set();
217
+
218
+ let content: string;
219
+ try {
220
+ content = fs.readFileSync(path.join(root, resourcePath), 'utf8');
221
+ } catch {
222
+ return new Set();
223
+ }
224
+
225
+ const methods = new Set<string>();
226
+ for (const block of content.matchAll(/@oagen-ignore-start[\s\S]*?@oagen-ignore-end/g)) {
227
+ for (const line of block[0].split('\n')) {
228
+ const match = line.match(/^\s{2}(?:(?:public|private|protected)\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(/);
229
+ if (match) methods.add(match[1]);
230
+ }
231
+ }
232
+ return methods;
233
+ }
234
+
235
+ function optionsObjectParam(method: BaselineMethod | undefined): OptionsObjectParam | undefined {
165
236
  if (!method || method.params.length !== 1) return undefined;
166
237
  const [param] = method.params;
167
238
  if (param.name !== 'options') return undefined;
168
239
  if (param.passingStyle && param.passingStyle !== 'options_object') return undefined;
169
240
  if (!param.type || /^(Record|object|any|unknown)\b/.test(param.type)) return undefined;
170
- return { name: param.name, type: param.type };
241
+ return { name: 'options', type: param.type, optional: param.optional === true, generated: false };
242
+ }
243
+
244
+ function methodOptionsName(method: string, resolvedServiceName: string): string {
245
+ if (method === 'list') return `${toPascalCase(resolvedServiceName)}ListOptions`;
246
+ return `${toPascalCase(method)}Options`;
247
+ }
248
+
249
+ function hiddenParamsFor(resolvedOp?: ResolvedOperation): Set<string> {
250
+ return new Set<string>([...Object.keys(getOpDefaults(resolvedOp)), ...getOpInferFromClient(resolvedOp)]);
251
+ }
252
+
253
+ function visibleQueryParamsForOptions(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation) {
254
+ const hidden = hiddenParamsFor(resolvedOp);
255
+ return op.queryParams.filter((param) => {
256
+ if (hidden.has(param.name)) return false;
257
+ if (plan.isPaginated && PAGINATION_PARAM_NAMES.has(param.name)) return false;
258
+ return true;
259
+ });
260
+ }
261
+
262
+ function optionsObjectShouldBeOptional(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation): boolean {
263
+ if (plan.hasBody) return false;
264
+ if (op.pathParams.length > 0) return false;
265
+ return visibleQueryParamsForOptions(op, plan, resolvedOp).every((param) => !param.required);
266
+ }
267
+
268
+ function operationHasOptionsInput(op: Operation, plan: OperationPlan, resolvedOp?: ResolvedOperation): boolean {
269
+ return (
270
+ op.pathParams.length > 0 ||
271
+ plan.hasBody ||
272
+ plan.isPaginated ||
273
+ visibleQueryParamsForOptions(op, plan, resolvedOp).length > 0
274
+ );
275
+ }
276
+
277
+ function optionsObjectInfo(
278
+ service: Service,
279
+ method: string,
280
+ op: Operation,
281
+ plan: OperationPlan,
282
+ ctx: EmitterContext,
283
+ baselineMethod: BaselineMethod | undefined,
284
+ resolvedOp?: ResolvedOperation,
285
+ ): OptionsObjectParam | undefined {
286
+ const baseline = optionsObjectParam(baselineMethod);
287
+ if (baseline) return baseline;
288
+
289
+ const overrideType = operationOverrideFor(ctx, op)?.optionsType;
290
+ if (overrideType) {
291
+ return {
292
+ name: 'options',
293
+ type: overrideType,
294
+ optional: optionsObjectShouldBeOptional(op, plan, resolvedOp),
295
+ generated: baselineTypeSourceFile(ctx, overrideType) === undefined,
296
+ };
297
+ }
298
+
299
+ if (!operationHasOptionsInput(op, plan, resolvedOp)) return undefined;
300
+
301
+ return {
302
+ name: 'options',
303
+ type: methodOptionsName(method, resolveResourceClassName(service, ctx)),
304
+ optional: optionsObjectShouldBeOptional(op, plan, resolvedOp),
305
+ generated: true,
306
+ };
307
+ }
308
+
309
+ function renderOptionsParam(param: OptionsObjectParam): string {
310
+ return `options${param.optional ? '?' : ''}: ${param.type}`;
171
311
  }
172
312
 
173
313
  function autoPaginatableItemType(returnType: string | undefined): string | undefined {
174
- return returnType?.match(/\bAutoPaginatable<\s*([A-Za-z_$][\w$]*)/)?.[1];
314
+ // Match both AutoPaginatable<T> and the legacy List<T> pattern so baseline
315
+ // item types are extracted even when the hand-written code predates AutoPaginatable.
316
+ return returnType?.match(/\b(?:AutoPaginatable|List)<\s*([A-Za-z_$][\w$]*)/)?.[1];
175
317
  }
176
318
 
177
319
  function baselineTypeSourceFile(ctx: EmitterContext, typeName: string): string | undefined {
@@ -207,9 +349,15 @@ function preferredBaselineTypeName(ctx: EmitterContext, typeName: string | undef
207
349
  }
208
350
 
209
351
  function preferredBaselineReturnType(ctx: EmitterContext, returnType: string | undefined): string | undefined {
352
+ if (!returnType) return undefined;
353
+ // Only preserve baseline return types that already use AutoPaginatable.
354
+ // Legacy patterns like List<T> can't be used as-is since the generated
355
+ // method body returns new AutoPaginatable(...).
356
+ if (!/\bAutoPaginatable\b/.test(returnType)) return undefined;
210
357
  const itemType = autoPaginatableItemType(returnType);
358
+ if (!itemType) return undefined;
211
359
  const preferred = preferredBaselineTypeName(ctx, itemType);
212
- if (!returnType || !itemType || !preferred || preferred === itemType) return returnType;
360
+ if (!preferred || preferred === itemType) return returnType;
213
361
  return returnType.replace(new RegExp(`\\b${itemType}\\b`, 'g'), preferred);
214
362
  }
215
363
 
@@ -317,21 +465,14 @@ function deduplicateMethodNames(
317
465
  }
318
466
 
319
467
  /**
320
- * Emit one interface file per paginated list operation that has extension
321
- * query params. Placing the options interface under `interfaces/` lets the
322
- * per-service barrel pick it up via `export * from './interfaces'`, which
323
- * is what the root `src/index.ts` re-exports. When the interface was
324
- * declared inline in the resource file, it was unreachable from the barrel
325
- * and callers couldn't import the type by name from the package root.
468
+ * Emit one interface/type file per generated options-object operation.
469
+ * Placing the options type under `interfaces/` lets the per-service barrel
470
+ * pick it up via `export * from './interfaces'`.
326
471
  */
327
- function generatePaginatedOptionsInterfaces(
328
- service: Service,
329
- ctx: EmitterContext,
330
- specEnumNames: Set<string>,
331
- ): GeneratedFile[] {
472
+ function generateOptionsInterfaces(service: Service, ctx: EmitterContext, specEnumNames: Set<string>): GeneratedFile[] {
332
473
  const files: GeneratedFile[] = [];
333
- const resolvedName = resolveResourceClassName(service, ctx);
334
474
  const serviceDir = resolveResourceDir(service, ctx);
475
+ const resolvedLookup = buildResolvedLookup(ctx);
335
476
 
336
477
  const plans = service.operations.map((op) => ({
337
478
  op,
@@ -340,28 +481,125 @@ function generatePaginatedOptionsInterfaces(
340
481
  }));
341
482
 
342
483
  for (const { op, plan, method } of plans) {
343
- if (!plan.isPaginated) continue;
344
- const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
345
- if (extraParams.length === 0) continue;
484
+ const resolvedOp = lookupResolved(op, resolvedLookup);
485
+ const baselineMethod = baselineMethodFor(service, method, ctx);
486
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
487
+ if (!optionInfo?.generated) continue;
488
+ if (baselineTypeSourceFile(ctx, optionInfo.type)) continue;
489
+
490
+ const optionsName = optionInfo.type;
491
+ const optionFileStem = `${fileName(optionsName)}.interface`;
492
+ const filePath = `src/${serviceDir}/interfaces/${optionFileStem}.ts`;
493
+ if (!liveSurfaceHasFile(filePath) || existingInterfaceBarrelExports(ctx, serviceDir, optionFileStem)) {
494
+ recordGeneratedOptionInterface(ctx, serviceDir, optionFileStem, optionsName);
495
+ }
346
496
 
347
- const optionsName = paginatedOptionsName(method, resolvedName);
348
- const filePath = `src/${serviceDir}/interfaces/${fileName(optionsName)}.interface.ts`;
497
+ const optEnums = new Set<string>();
498
+ const optModels = new Set<string>();
499
+ for (const param of [...op.pathParams, ...visibleQueryParamsForOptions(op, plan, resolvedOp)]) {
500
+ collectParamTypeRefs(param.type, optEnums, optModels);
501
+ }
502
+ const bodyInfo = extractRequestBodyType(op, ctx);
503
+ if (bodyInfo?.kind === 'model') {
504
+ const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
505
+ if (bodyModel) {
506
+ for (const field of bodyModel.fields) {
507
+ collectParamTypeRefs(field.type, optEnums, optModels);
508
+ }
509
+ }
510
+ } else if (bodyInfo?.kind === 'union') {
511
+ for (const name of bodyInfo.modelNames) {
512
+ optModels.add(name);
513
+ }
514
+ }
349
515
 
350
516
  const lines: string[] = [];
351
- lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
517
+ if (plan.isPaginated) {
518
+ lines.push("import type { PaginationOptions } from '../../common/interfaces/pagination-options.interface';");
519
+ }
520
+ const { modelToService, resolveDir } = createServiceDirResolver(ctx.spec.models, ctx.spec.services, ctx);
521
+ if (optEnums.size > 0) {
522
+ const enumToService = assignEnumsToServices(ctx.spec.enums, ctx.spec.services, ctx.spec.models, ctx);
523
+ for (const name of optEnums) {
524
+ if (!specEnumNames.has(name)) continue;
525
+ if (isInlineEnum(name)) continue;
526
+ const enumDir = resolveDir(enumToService.get(name));
527
+ const relPath =
528
+ enumDir === serviceDir
529
+ ? `./${fileName(name)}.interface`
530
+ : `../../${enumDir}/interfaces/${fileName(name)}.interface`;
531
+ lines.push(`import type { ${name} } from '${relPath}';`);
532
+ }
533
+ }
534
+ for (const name of optModels) {
535
+ const modelDir = resolveDir(modelToService.get(name));
536
+ const relPath =
537
+ modelDir === serviceDir
538
+ ? `./${fileName(name)}.interface`
539
+ : `../../${modelDir}/interfaces/${fileName(name)}.interface`;
540
+ lines.push(`import type { ${resolveInterfaceName(name, ctx)} } from '${relPath}';`);
541
+ }
352
542
  lines.push('');
353
- lines.push(`export interface ${optionsName} extends PaginationOptions {`);
354
- for (const param of extraParams) {
355
- const opt = !param.required ? '?' : '';
356
- if (param.description || param.deprecated) {
543
+
544
+ const headerParts: string[] = [];
545
+ const pushField = (name: string, required: boolean, type: string, description?: string, deprecated?: boolean) => {
546
+ const opt = !required ? '?' : '';
547
+ if (description || deprecated) {
357
548
  const parts: string[] = [];
358
- if (param.description) parts.push(param.description);
359
- if (param.deprecated) parts.push('@deprecated');
360
- lines.push(...docComment(parts.join('\n'), 2));
549
+ if (description) parts.push(description);
550
+ if (deprecated) parts.push('@deprecated');
551
+ headerParts.push(...docComment(parts.join('\n'), 2));
552
+ }
553
+ headerParts.push(` ${name}${opt}: ${type};`);
554
+ };
555
+
556
+ for (const param of op.pathParams) {
557
+ pushField(
558
+ fieldName(param.name),
559
+ true,
560
+ mapParamType(param.type, specEnumNames),
561
+ param.description,
562
+ param.deprecated,
563
+ );
564
+ }
565
+ for (const param of visibleQueryParamsForOptions(op, plan, resolvedOp)) {
566
+ pushField(
567
+ fieldName(param.name),
568
+ param.required,
569
+ mapParamType(param.type, specEnumNames),
570
+ param.description,
571
+ param.deprecated,
572
+ );
573
+ }
574
+
575
+ if (bodyInfo?.kind === 'model') {
576
+ const bodyModel = ctx.spec.models.find((m) => m.name === bodyInfo.name);
577
+ if (bodyModel) {
578
+ for (const field of bodyModel.fields) {
579
+ pushField(
580
+ fieldName(field.name),
581
+ field.required,
582
+ mapParamType(field.type, specEnumNames),
583
+ field.description,
584
+ field.deprecated,
585
+ );
586
+ }
587
+ }
588
+ lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
589
+ lines.push(...headerParts);
590
+ lines.push('}');
591
+ } else if (bodyInfo?.kind === 'union') {
592
+ const baseType = headerParts.length > 0 ? `{\n${headerParts.join('\n')}\n}` : '{}';
593
+ lines.push(`export type ${optionsName} = ${baseType} & (${bodyInfo.typeStr});`);
594
+ } else {
595
+ if (plan.isPaginated && headerParts.length === 0) {
596
+ lines.push(`export type ${optionsName} = PaginationOptions;`);
597
+ } else {
598
+ lines.push(`export interface ${optionsName}${plan.isPaginated ? ' extends PaginationOptions' : ''} {`);
599
+ lines.push(...headerParts);
600
+ lines.push('}');
361
601
  }
362
- lines.push(` ${fieldName(param.name)}${opt}: ${mapParamType(param.type, specEnumNames)};`);
363
602
  }
364
- lines.push('}');
365
603
 
366
604
  files.push({
367
605
  path: filePath,
@@ -435,7 +673,7 @@ export function generateResources(services: Service[], ctx: EmitterContext): Gen
435
673
  const isOwnedService = isNodeOwnedService(ctx, service.name, resolveResourceClassName(service, ctx));
436
674
  if (!isOwnedService && isServiceCoveredByExisting(service, ctx) && !hasMethodsAbsentFromBaseline(service, ctx))
437
675
  continue;
438
- files.push(...generatePaginatedOptionsInterfaces(service, ctx, topLevelEnumNames));
676
+ files.push(...generateOptionsInterfaces(service, ctx, topLevelEnumNames));
439
677
  }
440
678
 
441
679
  return files;
@@ -504,8 +742,12 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
504
742
  // Done after dedup + sort so the rendered methods keep their stable names.
505
743
  // Imports below filter through `plans`, so they automatically narrow to
506
744
  // the kept operations only — no orphan imports.
745
+ const ignoredMethodNames = ignoredResourceMethodNames(ctx, resourcePath);
507
746
  const baselineMethodNames = new Set(Object.keys(ctx.apiSurface?.classes?.[serviceClass]?.methods ?? {}));
508
747
  const planCountBeforeFilter = plans.length;
748
+ if (ignoredMethodNames.size > 0) {
749
+ plans = plans.filter((p) => !ignoredMethodNames.has(p.method));
750
+ }
509
751
  if (!isNodeOwnedService(ctx, service.name, serviceClass) && baselineMethodNames.size > 0) {
510
752
  plans = plans.filter((p) => !baselineMethodNames.has(p.method));
511
753
  }
@@ -520,38 +762,66 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
520
762
  }
521
763
 
522
764
  const hasPaginated = plans.some((p) => p.plan.isPaginated);
523
- const needsPaginationOptionsImport = plans.some(
524
- (p) =>
765
+ const resolvedLookup = buildResolvedLookup(ctx);
766
+ const needsPaginationOptionsImport = plans.some((p) => {
767
+ const baseline = baselineMethodFor(service, p.method, ctx);
768
+ const optionInfo = optionsObjectInfo(
769
+ service,
770
+ p.method,
771
+ p.op,
772
+ p.plan,
773
+ ctx,
774
+ baseline,
775
+ lookupResolved(p.op, resolvedLookup),
776
+ );
777
+ const extraParams = p.op.queryParams.filter((param) => !PAGINATION_PARAM_NAMES.has(param.name));
778
+ const needsWireSerializer = extraParams.some((param) => fieldName(param.name) !== wireFieldName(param.name));
779
+ return (
525
780
  p.plan.isPaginated &&
526
- (!optionsObjectParam(baselineMethodFor(service, p.method, ctx)) ||
781
+ (needsWireSerializer ||
782
+ !optionInfo ||
527
783
  /\bPaginationOptions\b/.test(
528
784
  preferredBaselineReturnType(ctx, baselineMethodFor(service, p.method, ctx)?.returnType) ?? '',
529
- )),
530
- );
785
+ ))
786
+ );
787
+ });
531
788
  const modelMap = new Map(ctx.spec.models.map((m) => [m.name, m]));
532
789
 
533
790
  // Collect models for imports — only include models that are actually used
534
791
  // in method signatures (not all union variants from the spec)
535
792
  const responseModels = new Set<string>();
793
+ const responseModelsForSignature = new Set<string>();
536
794
  const requestModels = new Set<string>();
537
795
  const requestModelsForSignature = new Set<string>();
538
796
  const paramEnums = new Set<string>();
539
797
  const paramModels = new Set<string>();
540
798
  const optionObjectTypes = new Set<string>();
799
+ const returnTypeImports = new Set<string>();
541
800
  const baselineResponseTypes = new Set<string>();
542
- for (const { op, plan } of plans) {
543
- const baselineMethod = baselineMethodFor(service, resolveMethodName(op, service, ctx), ctx);
544
- const existingOptions = optionsObjectParam(baselineMethod);
801
+ for (const { op, plan, method } of plans) {
802
+ const baselineMethod = baselineMethodFor(service, method, ctx);
803
+ const existingOptions = optionsObjectInfo(
804
+ service,
805
+ method,
806
+ op,
807
+ plan,
808
+ ctx,
809
+ baselineMethod,
810
+ lookupResolved(op, resolvedLookup),
811
+ );
545
812
  if (existingOptions) optionObjectTypes.add(existingOptions.type);
813
+ for (const typeName of operationOverrideFor(ctx, op)?.returnTypeImports ?? []) {
814
+ returnTypeImports.add(typeName);
815
+ }
546
816
 
547
- // Always collect param type refs for enums inline options interfaces
548
- // are generated for all methods (including baseline ones), so their
549
- // type dependencies must always be imported.
550
- const queryParams = plan.isPaginated
551
- ? op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name))
552
- : op.queryParams;
553
- for (const param of [...queryParams, ...op.pathParams]) {
554
- collectParamTypeRefs(param.type, paramEnums, paramModels);
817
+ // Collect param type refs only when params appear directly in this
818
+ // resource method. Options-object methods import a single options type;
819
+ // that generated interface owns its own enum/model imports.
820
+ if (!existingOptions) {
821
+ const queryParams = plan.isPaginated ? [] : op.queryParams;
822
+ for (const param of [...queryParams, ...op.pathParams]) {
823
+ collectParamTypeRefs(param.type, paramEnums, paramModels);
824
+ }
555
825
  }
556
826
 
557
827
  // Always collect imports for every rendered method. Earlier versions
@@ -581,8 +851,14 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
581
851
  }
582
852
  }
583
853
  responseModels.add(itemName);
854
+ responseModelsForSignature.add(itemName);
584
855
  } else if (plan.responseModelName) {
585
856
  responseModels.add(plan.responseModelName);
857
+ const override = operationOverrideFor(ctx, op);
858
+ const resolvedResponseName = resolveInterfaceName(plan.responseModelName, ctx);
859
+ if (!override?.returnType || override.returnType.includes(resolvedResponseName)) {
860
+ responseModelsForSignature.add(plan.responseModelName);
861
+ }
586
862
  }
587
863
  // Import request body model(s) — handles both single models and union variants.
588
864
  const bodyInfo = extractRequestBodyType(op, ctx);
@@ -613,7 +889,6 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
613
889
  // Also collect models referenced in wrapper param signatures (e.g.,
614
890
  // `redirect_uris: RedirectUriInput[]`) — otherwise the wrapper emits a
615
891
  // reference to a type it never imported.
616
- const resolvedLookup = buildResolvedLookup(ctx);
617
892
  for (const { op } of plans) {
618
893
  const resolved = lookupResolved(op, resolvedLookup);
619
894
  if (resolved) {
@@ -628,7 +903,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
628
903
  }
629
904
  }
630
905
 
631
- const allModels = new Set([...responseModels, ...requestModelsForSignature, ...paramModels]);
906
+ const allModels = new Set([...responseModelsForSignature, ...requestModelsForSignature, ...paramModels]);
632
907
 
633
908
  const lines: string[] = [];
634
909
 
@@ -641,16 +916,10 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
641
916
  lines.push("import { AutoPaginatable } from '../common/utils/pagination';");
642
917
  lines.push("import { fetchAndDeserialize } from '../common/utils/fetch-and-deserialize';");
643
918
  }
644
-
645
- // Paginated list options live in their own interface files so they're
646
- // picked up by the per-service barrel (and flow through to the root
647
- // package barrel). Import them here rather than declaring inline.
648
- for (const { op, plan, method } of plans) {
649
- if (!plan.isPaginated) continue;
650
- const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
651
- if (extraParams.length === 0) continue;
652
- const optionsName = paginatedOptionsName(method, resolvedName);
653
- lines.push(`import type { ${optionsName} } from './interfaces/${fileName(optionsName)}.interface';`);
919
+ const shouldEmitVaultCryptoHelpers =
920
+ serviceClass === 'Vault' && !ignoredMethodNames.has('encrypt') && !ignoredMethodNames.has('decrypt');
921
+ if (shouldEmitVaultCryptoHelpers) {
922
+ lines.push("import { base64ToUint8Array, uint8ArrayToBase64 } from '../common/utils/base64';");
654
923
  }
655
924
 
656
925
  // Check if any operation needs PostOptions (idempotent POST or custom encoding)
@@ -666,7 +935,18 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
666
935
  for (const optionType of optionObjectTypes) {
667
936
  if (importedTypeNames.has(optionType)) continue;
668
937
  importedTypeNames.add(optionType);
669
- lines.push(`import type { ${optionType} } from './interfaces/${fileName(optionType)}.interface';`);
938
+ const sourceFile = baselineTypeSourceFile(ctx, optionType);
939
+ const relPath = sourceFile
940
+ ? relativeImport(resourcePath, sourceFile)
941
+ : `./interfaces/${fileName(optionType)}.interface`;
942
+ lines.push(`import type { ${optionType} } from '${relPath}';`);
943
+ }
944
+ for (const typeName of returnTypeImports) {
945
+ if (importedTypeNames.has(typeName)) continue;
946
+ const sourceFile = baselineTypeSourceFile(ctx, typeName) ?? liveSurfaceInterfacePath(typeName);
947
+ if (!sourceFile) continue;
948
+ importedTypeNames.add(typeName);
949
+ lines.push(`import type { ${typeName} } from '${relativeImport(resourcePath, sourceFile)}';`);
670
950
  }
671
951
 
672
952
  // Compute model-to-service mapping for correct cross-service import paths
@@ -728,6 +1008,22 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
728
1008
  importedTypeNames.add(wireName);
729
1009
  }
730
1010
 
1011
+ for (const name of responseModels) {
1012
+ if (allModels.has(name)) continue;
1013
+ const resolved = resolveInterfaceName(name, ctx);
1014
+ if (!usedWireTypes.has(resolved)) continue;
1015
+ const wireName = wireInterfaceName(resolved);
1016
+ if (importedTypeNames.has(wireName)) continue;
1017
+ const modelDir = modelToService.get(name);
1018
+ const modelServiceDir = resolveDir(modelDir);
1019
+ const relPath =
1020
+ modelServiceDir === serviceDir
1021
+ ? `./interfaces/${fileName(name)}.interface`
1022
+ : `../${modelServiceDir}/interfaces/${fileName(name)}.interface`;
1023
+ lines.push(`import type { ${wireName} } from '${relPath}';`);
1024
+ importedTypeNames.add(wireName);
1025
+ }
1026
+
731
1027
  for (const name of baselineResponseTypes) {
732
1028
  if (importedTypeNames.has(name)) continue;
733
1029
  const wireName = wireInterfaceName(name);
@@ -836,10 +1132,13 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
836
1132
  // in separate files under `interfaces/` so the per-service barrel can
837
1133
  // re-export them; see the earlier import block at the top of the file.
838
1134
  for (const { op, plan, method } of plans) {
1135
+ const resolved = lookupResolved(op, resolvedLookup);
1136
+ const baselineMethod = baselineMethodFor(service, method, ctx);
1137
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolved);
839
1138
  if (plan.isPaginated) {
840
1139
  const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
841
1140
  if (extraParams.length > 0) {
842
- const optionsName = paginatedOptionsName(method, resolvedName);
1141
+ const optionsName = optionInfo?.type ?? paginatedOptionsName(method, resolvedName);
843
1142
 
844
1143
  // When any extension param has a camelCase domain name that differs
845
1144
  // from its snake_case wire name, emit a serializer that translates
@@ -855,7 +1154,11 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
855
1154
  // extension fields land on top and the camelCase keys don't also
856
1155
  // leak into the query string.
857
1156
  lines.push(' const wire: Record<string, unknown> = {');
1157
+ const baselineFields = (
1158
+ ctx.apiSurface?.interfaces as Record<string, { fields?: Record<string, unknown> }> | undefined
1159
+ )?.[optionsName]?.fields;
858
1160
  for (const p of PAGINATION_PARAM_NAMES) {
1161
+ if (baselineFields && !(p in baselineFields) && !baselineFields[toCamelCase(p)]) continue;
859
1162
  lines.push(` ${p}: options.${p},`);
860
1163
  }
861
1164
  lines.push(' };');
@@ -869,7 +1172,7 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
869
1172
  lines.push('');
870
1173
  }
871
1174
  }
872
- } else if (!plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
1175
+ } else if (!optionInfo && !plan.isPaginated && !plan.hasBody && !plan.isDelete && op.queryParams.length > 0) {
873
1176
  // Non-paginated GET or void methods with query params get a typed options interface
874
1177
  // instead of falling back to Record<string, unknown>.
875
1178
  // Filter out hidden params (defaults and inferFromClient fields)
@@ -898,6 +1201,16 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
898
1201
  }
899
1202
  }
900
1203
 
1204
+ if (shouldEmitVaultCryptoHelpers) {
1205
+ lines.push('export interface VaultEncryptedData {');
1206
+ lines.push(' ciphertext: string;');
1207
+ lines.push(' encryptedKeys: string;');
1208
+ lines.push(' iv: string;');
1209
+ lines.push(' tag: string;');
1210
+ lines.push('}');
1211
+ lines.push('');
1212
+ }
1213
+
901
1214
  // Resource class
902
1215
  if (service.description) {
903
1216
  lines.push(...docComment(service.description));
@@ -916,11 +1229,62 @@ function generateResourceClass(service: Service, ctx: EmitterContext): Generated
916
1229
  }
917
1230
  }
918
1231
 
1232
+ if (shouldEmitVaultCryptoHelpers) {
1233
+ lines.push('');
1234
+ lines.push(...renderVaultCryptoMethods());
1235
+ }
1236
+
919
1237
  lines.push('}');
920
1238
 
921
1239
  return { path: resourcePath, content: lines.join('\n'), skipIfExists: true };
922
1240
  }
923
1241
 
1242
+ function renderVaultCryptoMethods(): string[] {
1243
+ return [
1244
+ ' async encrypt(',
1245
+ ' plaintext: string,',
1246
+ ' context: Record<string, string>,',
1247
+ ' aad?: string,',
1248
+ ' ): Promise<VaultEncryptedData> {',
1249
+ ' const dataKeyPair = await this.createDataKey({ context });',
1250
+ ' const encodedPlaintext = new TextEncoder().encode(plaintext);',
1251
+ ' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
1252
+ ' const encrypted = await this.workos.getCryptoProvider().encrypt(',
1253
+ ' encodedPlaintext,',
1254
+ ' base64ToUint8Array(dataKeyPair.dataKey.key),',
1255
+ ' undefined,',
1256
+ ' encodedAad,',
1257
+ ' );',
1258
+ '',
1259
+ ' return {',
1260
+ ' ciphertext: uint8ArrayToBase64(encrypted.ciphertext),',
1261
+ ' encryptedKeys: dataKeyPair.encryptedKeys,',
1262
+ ' iv: uint8ArrayToBase64(encrypted.iv),',
1263
+ ' tag: uint8ArrayToBase64(encrypted.tag),',
1264
+ ' };',
1265
+ ' }',
1266
+ '',
1267
+ ' async decrypt(',
1268
+ ' encryptedData: VaultEncryptedData,',
1269
+ ' aad?: string,',
1270
+ ' ): Promise<string> {',
1271
+ ' const dataKey = await this.decryptDataKey({',
1272
+ ' keys: encryptedData.encryptedKeys,',
1273
+ ' });',
1274
+ ' const encodedAad = aad ? new TextEncoder().encode(aad) : undefined;',
1275
+ ' const decrypted = await this.workos.getCryptoProvider().decrypt(',
1276
+ ' base64ToUint8Array(encryptedData.ciphertext),',
1277
+ ' base64ToUint8Array(dataKey.key),',
1278
+ ' base64ToUint8Array(encryptedData.iv),',
1279
+ ' base64ToUint8Array(encryptedData.tag),',
1280
+ ' encodedAad,',
1281
+ ' );',
1282
+ '',
1283
+ ' return new TextDecoder().decode(decrypted);',
1284
+ ' }',
1285
+ ];
1286
+ }
1287
+
924
1288
  function renderMethod(
925
1289
  op: Operation,
926
1290
  plan: OperationPlan,
@@ -948,9 +1312,12 @@ function renderMethod(
948
1312
  // otherwise compute from what the render path will actually include.
949
1313
  const httpKey = `${op.httpMethod.toUpperCase()} ${op.path}`;
950
1314
  const baselineClassMethod = baselineMethodFor(service, method, ctx);
1315
+ const optionInfo = optionsObjectInfo(service, method, op, plan, ctx, baselineClassMethod, resolvedOp);
951
1316
  const overlayMethod = ctx.overlayLookup?.methodByOperation?.get(httpKey) ?? baselineClassMethod;
952
1317
  let validParamNames: Set<string> | null = null;
953
- if (overlayMethod) {
1318
+ if (optionInfo) {
1319
+ validParamNames = new Set(['options']);
1320
+ } else if (overlayMethod) {
954
1321
  validParamNames = new Set(overlayMethod.params.map((p) => p.name));
955
1322
  } else {
956
1323
  // Compute actual params based on render path to avoid documenting params
@@ -1148,7 +1515,9 @@ function renderMethod(
1148
1515
  const unwrapped = unwrapListModel(pModel, modelMap);
1149
1516
  if (unwrapped) itemRawName = unwrapped.name;
1150
1517
  }
1151
- const itemTypeName = resolveInterfaceName(itemRawName, ctx);
1518
+ const baselineItemType =
1519
+ autoPaginatableItemType(baselineClassMethod?.returnType) ?? autoPaginatableItemType(overlayMethod?.returnType);
1520
+ const itemTypeName = preferredBaselineTypeName(ctx, baselineItemType) ?? resolveInterfaceName(itemRawName, ctx);
1152
1521
  docParts.push(`@returns {Promise<AutoPaginatable<${itemTypeName}>>}`);
1153
1522
  } else if (responseModel) {
1154
1523
  const returnTypeDoc = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
@@ -1185,7 +1554,20 @@ function renderMethod(
1185
1554
  }
1186
1555
  }
1187
1556
 
1188
- if (renderOptionsObjectMethod(lines, op, plan, method, service, ctx, modelMap, specEnumNames, baselineClassMethod)) {
1557
+ if (
1558
+ renderOptionsObjectMethod(
1559
+ lines,
1560
+ op,
1561
+ plan,
1562
+ method,
1563
+ service,
1564
+ ctx,
1565
+ modelMap,
1566
+ specEnumNames,
1567
+ baselineClassMethod,
1568
+ resolvedOp,
1569
+ )
1570
+ ) {
1189
1571
  return lines;
1190
1572
  }
1191
1573
 
@@ -1257,13 +1639,25 @@ function renderOptionsObjectMethod(
1257
1639
  modelMap: Map<string, Model>,
1258
1640
  specEnumNames: Set<string> | undefined,
1259
1641
  baselineMethod: BaselineMethod | undefined,
1642
+ resolvedOp: ResolvedOperation | undefined,
1260
1643
  ): boolean {
1261
- const optionParam = optionsObjectParam(baselineMethod);
1644
+ const optionParam = optionsObjectInfo(service, method, op, plan, ctx, baselineMethod, resolvedOp);
1262
1645
  if (!optionParam) return false;
1263
1646
 
1264
1647
  const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
1265
1648
  const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
1266
1649
  const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
1650
+ const override = operationOverrideFor(ctx, op);
1651
+ const bodyFieldMap = override?.bodyFieldMap;
1652
+ const visibleQueryParams = visibleQueryParamsForOptions(op, plan, resolvedOp);
1653
+ const queryBindings = visibleQueryParams.map((param) => fieldName(param.name));
1654
+ const hasQuery =
1655
+ visibleQueryParams.length > 0 ||
1656
+ Object.keys(getOpDefaults(resolvedOp)).length > 0 ||
1657
+ getOpInferFromClient(resolvedOp).length > 0;
1658
+ const queryOptionsArg = hasQuery
1659
+ ? `, { query: ${renderQueryExprWithOptions(visibleQueryParams, optionParam.optional, resolvedOp)} }`
1660
+ : '';
1267
1661
 
1268
1662
  if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
1269
1663
  let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
@@ -1281,14 +1675,19 @@ function renderOptionsObjectMethod(
1281
1675
  const wireType = wireInterfaceName(itemType);
1282
1676
  const returnType =
1283
1677
  preferredBaselineReturnType(ctx, baselineMethod?.returnType) ?? `Promise<AutoPaginatable<${itemType}>>`;
1284
- lines.push(` async ${method}(options: ${optionParam.type}): ${returnType} {`);
1285
- renderOptionsObjectDestructure(lines, pathBindings, 'paginationOptions');
1678
+ const extraParams = op.queryParams.filter((p) => !PAGINATION_PARAM_NAMES.has(p.name));
1679
+ const needsWireSerializer = extraParams.some((p) => fieldName(p.name) !== wireFieldName(p.name));
1680
+ const listOptionsExpr = needsWireSerializer
1681
+ ? `options ? serialize${optionParam.type}(options) : undefined`
1682
+ : 'paginationOptions';
1683
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${returnType} {`);
1684
+ renderOptionsObjectDestructure(lines, pathBindings, needsWireSerializer ? undefined : 'paginationOptions');
1286
1685
  lines.push(` return new AutoPaginatable(`);
1287
1686
  lines.push(` await fetchAndDeserialize<${wireType}, ${itemType}>(`);
1288
1687
  lines.push(` this.workos,`);
1289
1688
  lines.push(` ${pathStr},`);
1290
1689
  lines.push(` deserialize${itemType},`);
1291
- lines.push(` paginationOptions,`);
1690
+ lines.push(` ${listOptionsExpr},`);
1292
1691
  lines.push(` ),`);
1293
1692
  lines.push(` (params) =>`);
1294
1693
  lines.push(` fetchAndDeserialize<${wireType}, ${itemType}>(`);
@@ -1297,16 +1696,16 @@ function renderOptionsObjectMethod(
1297
1696
  lines.push(` deserialize${itemType},`);
1298
1697
  lines.push(` params,`);
1299
1698
  lines.push(` ),`);
1300
- lines.push(` paginationOptions,`);
1699
+ lines.push(` ${listOptionsExpr},`);
1301
1700
  lines.push(` );`);
1302
1701
  lines.push(' }');
1303
1702
  return true;
1304
1703
  }
1305
1704
 
1306
1705
  if (plan.isDelete && !plan.hasBody) {
1307
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1706
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1308
1707
  renderOptionsObjectDestructure(lines, pathBindings);
1309
- lines.push(` await this.workos.delete(${pathStr});`);
1708
+ lines.push(` await this.workos.delete(${pathStr}${queryOptionsArg});`);
1310
1709
  lines.push(' }');
1311
1710
  return true;
1312
1711
  }
@@ -1333,49 +1732,71 @@ function renderOptionsObjectMethod(
1333
1732
  }
1334
1733
 
1335
1734
  if (plan.isDelete) {
1336
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1337
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1338
- lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr});`);
1735
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1736
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1737
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1738
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1739
+ lines.push(` await this.workos.deleteWithBody<${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`);
1339
1740
  lines.push(' }');
1340
1741
  return true;
1341
1742
  }
1342
1743
 
1343
1744
  if (!responseModel) {
1344
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<void> {`);
1345
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1346
- lines.push(` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr});`);
1745
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): Promise<void> {`);
1746
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1747
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1748
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1749
+ lines.push(
1750
+ ` await this.workos.${op.httpMethod}<void, ${entityType}>(${pathStr}, ${bodyExpr}${queryOptionsArg});`,
1751
+ );
1347
1752
  lines.push(' }');
1348
1753
  return true;
1349
1754
  }
1350
1755
 
1351
1756
  const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1757
+ const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
1352
1758
  const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1353
1759
  const returnExpr = plan.isArrayResponse
1354
1760
  ? `data.map(deserialize${responseModel})`
1355
1761
  : `deserialize${responseModel}(data)`;
1762
+ const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
1356
1763
 
1357
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1358
- renderOptionsObjectDestructure(lines, pathBindings, 'payload');
1764
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
1765
+ renderOptionsObjectDestructure(lines, [...pathBindings, ...queryBindings], 'payload');
1766
+ const bodyParam = renderOptionsObjectBodyFieldMap(lines, bodyFieldMap);
1767
+ bodyExpr = bodyArgExprWithParam(bodyExpr, bodyParam);
1359
1768
  lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}, ${entityType}>(`);
1360
1769
  lines.push(` ${pathStr},`);
1361
- lines.push(` ${bodyExpr},`);
1770
+ lines.push(` ${bodyExpr}${queryOptionsArg},`);
1362
1771
  lines.push(' );');
1363
- lines.push(` return ${returnExpr};`);
1772
+ if (override?.returnExpression) {
1773
+ lines.push(` const result = ${returnExpr};`);
1774
+ lines.push(` return ${override.returnExpression};`);
1775
+ } else {
1776
+ lines.push(` return ${finalReturnExpr};`);
1777
+ }
1364
1778
  lines.push(' }');
1365
1779
  return true;
1366
1780
  }
1367
1781
 
1368
1782
  if (responseModel) {
1369
1783
  const returnType = plan.isArrayResponse ? `${responseModel}[]` : responseModel;
1784
+ const methodReturnType = override?.returnType ?? `Promise<${returnType}>`;
1370
1785
  const wireType = plan.isArrayResponse ? `${wireInterfaceName(responseModel)}[]` : wireInterfaceName(responseModel);
1371
1786
  const returnExpr = plan.isArrayResponse
1372
1787
  ? `data.map(deserialize${responseModel})`
1373
1788
  : `deserialize${responseModel}(data)`;
1789
+ const finalReturnExpr = override?.returnDataProperty ? `${returnExpr}.${override.returnDataProperty}` : returnExpr;
1374
1790
 
1375
- lines.push(` async ${method}(options: ${optionParam.type}): Promise<${returnType}> {`);
1791
+ lines.push(` async ${method}(${renderOptionsParam(optionParam)}): ${methodReturnType} {`);
1376
1792
  renderOptionsObjectDestructure(lines, pathBindings);
1377
- lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr});`);
1378
- lines.push(` return ${returnExpr};`);
1793
+ lines.push(` const { data } = await this.workos.${op.httpMethod}<${wireType}>(${pathStr}${queryOptionsArg});`);
1794
+ if (override?.returnExpression) {
1795
+ lines.push(` const result = ${returnExpr};`);
1796
+ lines.push(` return ${override.returnExpression};`);
1797
+ } else {
1798
+ lines.push(` return ${finalReturnExpr};`);
1799
+ }
1379
1800
  lines.push(' }');
1380
1801
  return true;
1381
1802
  }
@@ -1393,6 +1814,23 @@ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[],
1393
1814
  }
1394
1815
  }
1395
1816
 
1817
+ function renderOptionsObjectBodyFieldMap(lines: string[], bodyFieldMap: Record<string, string> | undefined): string {
1818
+ const entries = Object.entries(bodyFieldMap ?? {});
1819
+ if (entries.length === 0) return 'payload';
1820
+
1821
+ lines.push(' const requestPayload = {');
1822
+ lines.push(' ...payload,');
1823
+ for (const [source, target] of entries) {
1824
+ lines.push(` ${target}: payload.${source},`);
1825
+ }
1826
+ lines.push(' };');
1827
+ return 'requestPayload';
1828
+ }
1829
+
1830
+ function bodyArgExprWithParam(bodyExpr: string, paramName: string): string {
1831
+ return paramName === 'payload' ? bodyExpr : bodyExpr.replace(/\bpayload\b/g, paramName);
1832
+ }
1833
+
1396
1834
  function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
1397
1835
  // Return resolved SDK field names directly — the URL template uses these
1398
1836
  // names too (via the param-name map threaded into buildNodePathExpression),
@@ -1464,7 +1902,7 @@ function renderPaginatedMethod(
1464
1902
  lines.push(` deserialize${itemType},`);
1465
1903
  lines.push(` params,`);
1466
1904
  lines.push(` ),`);
1467
- lines.push(` options,`);
1905
+ lines.push(` ${serializeCall},`);
1468
1906
  lines.push(` );`);
1469
1907
  lines.push(' }');
1470
1908
  }
@@ -1862,6 +2300,32 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
1862
2300
  return `options ? { ${parts.join(', ')} } : undefined`;
1863
2301
  }
1864
2302
 
2303
+ function renderQueryExprWithOptions(
2304
+ queryParams: { name: string; required: boolean }[],
2305
+ optional: boolean,
2306
+ resolvedOp?: ResolvedOperation,
2307
+ ): string {
2308
+ const parts: string[] = [];
2309
+ for (const param of queryParams) {
2310
+ const camel = fieldName(param.name);
2311
+ const snake = wireFieldName(param.name);
2312
+ const access = optional ? `options?.${camel}` : `options.${camel}`;
2313
+ if (param.required) {
2314
+ parts.push(`${snake}: ${access}`);
2315
+ } else {
2316
+ parts.push(`...(${access} !== undefined && { ${snake}: ${access} })`);
2317
+ }
2318
+ }
2319
+ for (const [key, value] of Object.entries(getOpDefaults(resolvedOp))) {
2320
+ parts.push(`${key}: ${tsLiteral(value)}`);
2321
+ }
2322
+ for (const field of getOpInferFromClient(resolvedOp)) {
2323
+ parts.push(`${field}: ${clientFieldExpression(field)}`);
2324
+ }
2325
+ if (parts.length === 0) return optional ? 'options' : '{}';
2326
+ return `{ ${parts.join(', ')} }`;
2327
+ }
2328
+
1865
2329
  function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
1866
2330
  return buildNodePathExpression(op.path, paramNameMap);
1867
2331
  }