@workos/oagen-emitters 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.oxfmtrc.json +8 -1
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +129 -0
  6. package/dist/index.d.mts +10 -1
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +11943 -2728
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +298 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +137 -46
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-php.ts +28 -26
  23. package/smoke/sdk-python.ts +5 -2
  24. package/smoke/sdk-ruby.ts +17 -3
  25. package/smoke/sdk-rust.ts +16 -3
  26. package/src/go/client.ts +141 -0
  27. package/src/go/enums.ts +196 -0
  28. package/src/go/fixtures.ts +212 -0
  29. package/src/go/index.ts +81 -0
  30. package/src/go/manifest.ts +36 -0
  31. package/src/go/models.ts +254 -0
  32. package/src/go/naming.ts +191 -0
  33. package/src/go/resources.ts +827 -0
  34. package/src/go/tests.ts +751 -0
  35. package/src/go/type-map.ts +82 -0
  36. package/src/go/wrappers.ts +261 -0
  37. package/src/index.ts +3 -0
  38. package/src/node/client.ts +167 -122
  39. package/src/node/enums.ts +13 -4
  40. package/src/node/errors.ts +42 -233
  41. package/src/node/field-plan.ts +726 -0
  42. package/src/node/fixtures.ts +15 -5
  43. package/src/node/index.ts +65 -16
  44. package/src/node/models.ts +264 -96
  45. package/src/node/naming.ts +52 -25
  46. package/src/node/resources.ts +621 -172
  47. package/src/node/sdk-errors.ts +41 -0
  48. package/src/node/tests.ts +71 -27
  49. package/src/node/type-map.ts +4 -2
  50. package/src/node/utils.ts +56 -64
  51. package/src/node/wrappers.ts +151 -0
  52. package/src/php/client.ts +171 -0
  53. package/src/php/enums.ts +67 -0
  54. package/src/php/errors.ts +9 -0
  55. package/src/php/fixtures.ts +181 -0
  56. package/src/php/index.ts +96 -0
  57. package/src/php/manifest.ts +36 -0
  58. package/src/php/models.ts +310 -0
  59. package/src/php/naming.ts +298 -0
  60. package/src/php/resources.ts +561 -0
  61. package/src/php/tests.ts +533 -0
  62. package/src/php/type-map.ts +90 -0
  63. package/src/php/utils.ts +18 -0
  64. package/src/php/wrappers.ts +151 -0
  65. package/src/python/client.ts +337 -0
  66. package/src/python/enums.ts +313 -0
  67. package/src/python/fixtures.ts +196 -0
  68. package/src/python/index.ts +95 -0
  69. package/src/python/manifest.ts +38 -0
  70. package/src/python/models.ts +688 -0
  71. package/src/python/naming.ts +209 -0
  72. package/src/python/resources.ts +1322 -0
  73. package/src/python/tests.ts +1335 -0
  74. package/src/python/type-map.ts +93 -0
  75. package/src/python/wrappers.ts +191 -0
  76. package/src/shared/model-utils.ts +255 -0
  77. package/src/shared/naming-utils.ts +107 -0
  78. package/src/shared/non-spec-services.ts +54 -0
  79. package/src/shared/resolved-ops.ts +109 -0
  80. package/src/shared/wrapper-utils.ts +59 -0
  81. package/test/go/client.test.ts +92 -0
  82. package/test/go/enums.test.ts +132 -0
  83. package/test/go/errors.test.ts +9 -0
  84. package/test/go/models.test.ts +265 -0
  85. package/test/go/resources.test.ts +408 -0
  86. package/test/go/tests.test.ts +143 -0
  87. package/test/node/client.test.ts +199 -94
  88. package/test/node/enums.test.ts +75 -3
  89. package/test/node/errors.test.ts +2 -41
  90. package/test/node/models.test.ts +109 -20
  91. package/test/node/naming.test.ts +37 -4
  92. package/test/node/resources.test.ts +662 -30
  93. package/test/node/serializers.test.ts +36 -7
  94. package/test/node/type-map.test.ts +11 -0
  95. package/test/php/client.test.ts +94 -0
  96. package/test/php/enums.test.ts +173 -0
  97. package/test/php/errors.test.ts +9 -0
  98. package/test/php/models.test.ts +497 -0
  99. package/test/php/resources.test.ts +644 -0
  100. package/test/php/tests.test.ts +118 -0
  101. package/test/python/client.test.ts +200 -0
  102. package/test/python/enums.test.ts +228 -0
  103. package/test/python/errors.test.ts +16 -0
  104. package/test/python/manifest.test.ts +74 -0
  105. package/test/python/models.test.ts +716 -0
  106. package/test/python/resources.test.ts +617 -0
  107. package/test/python/tests.test.ts +202 -0
  108. package/src/node/common.ts +0 -273
  109. package/src/node/config.ts +0 -71
  110. package/src/node/serializers.ts +0 -744
package/smoke/sdk-go.ts CHANGED
@@ -140,6 +140,43 @@ function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
140
140
  return manifest;
141
141
  }
142
142
 
143
+ // ---------------------------------------------------------------------------
144
+ // Accessor map -- discover actual method names from the generated workos.go
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function buildAccessorMap(sdkPath: string): Map<string, string> {
148
+ const map = new Map<string, string>();
149
+ // Find the main workos.go or *.go file that has Client methods
150
+ const candidates = ['workos.go'];
151
+ for (const fname of candidates) {
152
+ const fpath = resolve(sdkPath, fname);
153
+ if (!existsSync(fpath)) continue;
154
+ const content = readFileSync(fpath, 'utf-8');
155
+ // Match: func (c *Client) ServiceName() *serviceNameService {
156
+ const re = /func \(c \*Client\) (\w+)\(\)/g;
157
+ let m;
158
+ while ((m = re.exec(content)) !== null) {
159
+ const accessor = m[1];
160
+ // Map by lowercase key for case-insensitive matching
161
+ map.set(accessor.toLowerCase(), accessor);
162
+ }
163
+ }
164
+ return map;
165
+ }
166
+
167
+ /**
168
+ * Resolve the Go service accessor name from the manifest service name.
169
+ * Uses the accessor map (built from the generated SDK) for exact matching.
170
+ * Falls back to PascalCase for services not found in the map.
171
+ */
172
+ function resolveAccessorName(manifestService: string, accessorMap: Map<string, string>): string {
173
+ // Try case-insensitive lookup: "sso" -> "SSO", "api_keys" -> "ApiKeys"
174
+ const pascalized = toPascalCase(manifestService);
175
+ const found = accessorMap.get(pascalized.toLowerCase());
176
+ if (found) return found;
177
+ return pascalized;
178
+ }
179
+
143
180
  // ---------------------------------------------------------------------------
144
181
  // Method resolution
145
182
  // ---------------------------------------------------------------------------
@@ -257,7 +294,12 @@ class CaptureProxy {
257
294
  }
258
295
  }
259
296
 
260
- const capturedReq: CapturedRequest = { method, path, queryParams, body: parsedReqBody };
297
+ const capturedReq: CapturedRequest = {
298
+ method,
299
+ path,
300
+ queryParams,
301
+ body: parsedReqBody,
302
+ };
261
303
 
262
304
  // Forward to the real API
263
305
  const targetUrl = new URL(path, this.targetBaseUrl);
@@ -340,15 +382,16 @@ function detectModulePath(sdkPath: string): string {
340
382
  const match = goMod.match(/^module\s+(\S+)/m);
341
383
  if (match) return match[1];
342
384
  }
343
- return 'github.com/workos/workos-go/v4';
385
+ return 'github.com/workos/workos-go/v2';
344
386
  }
345
387
 
346
388
  function generateGoImports(
347
389
  modulePath: string,
348
- servicePackages: Set<string>,
390
+ _servicePackages: Set<string>,
349
391
  needsJson: boolean,
350
- needsServicePkg: boolean,
392
+ _needsServicePkg: boolean,
351
393
  ): string {
394
+ // New emitter: flat package -- everything lives in the root module, no sub-packages.
352
395
  const lines: string[] = [];
353
396
  lines.push('import (');
354
397
  lines.push('\t"context"');
@@ -358,20 +401,21 @@ function generateGoImports(
358
401
  lines.push('\t"fmt"');
359
402
  lines.push('\t"os"');
360
403
  lines.push('');
361
- lines.push(`\tworkos "${modulePath}/pkg"`);
362
- if (needsServicePkg) {
363
- for (const pkg of [...servicePackages].sort()) {
364
- lines.push(`\t"${modulePath}/pkg/${pkg}"`);
365
- }
366
- }
404
+ lines.push(`\t"${modulePath}"`);
367
405
  lines.push(')');
368
406
  return lines.join('\n');
369
407
  }
370
408
 
371
- function generateGoPayloadStruct(payload: Record<string, unknown>, optsType: string, servicePackage: string): string {
409
+ function generateGoPayloadStruct(payload: Record<string, unknown>, paramsType: string): string {
410
+ // New emitter: all types in root workos package, params are pointers.
411
+ // Skip nested objects, arrays, and nil values since Go requires typed structs
412
+ // and primitive fields can't be nil.
372
413
  const lines: string[] = [];
373
- lines.push(`${servicePackage}.${optsType}{`);
414
+ lines.push(`&workos.${paramsType}{`);
374
415
  for (const [key, value] of Object.entries(payload)) {
416
+ // Skip nil, nested objects, and arrays
417
+ if (value === null || value === undefined) continue;
418
+ if (typeof value === 'object') continue;
375
419
  const goField = goFieldName(key);
376
420
  lines.push(`\t\t${goField}: ${goLiteral(value)},`);
377
421
  }
@@ -409,9 +453,9 @@ function generateGoCallBlock(
409
453
  pathParams: Record<string, string>,
410
454
  spec: any,
411
455
  callIndex: number,
456
+ accessorMap: Map<string, string>,
412
457
  ): string {
413
458
  const lines: string[] = [];
414
- const servicePackage = goServicePackageName(resolution.service);
415
459
  const method = resolution.method;
416
460
 
417
461
  // Build arguments
@@ -422,52 +466,63 @@ function generateGoCallBlock(
422
466
  args.push(`"${pathParams[p.name] || ''}"`);
423
467
  }
424
468
 
425
- // Request body opts struct
469
+ // Build service-prefixed params struct name (matches emitter's paramsStructName)
470
+ const servicePrefix = goExportedName(resolution.service);
471
+ const paramsTypeName = method.startsWith(servicePrefix) ? `${method}Params` : `${servicePrefix}${method}Params`;
472
+
473
+ // Request body params struct (emitter uses &workos.{ServicePrefix}{Method}Params{...})
474
+ const hasQueryParams = op.queryParams && op.queryParams.length > 0;
426
475
  if (op.requestBody) {
427
476
  const payload = generatePayload(op, spec);
428
477
  if (payload && Object.keys(payload).length > 0) {
429
- const optsType = `${method}Opts`;
430
- args.push(generateGoPayloadStruct(payload, optsType, servicePackage));
431
- }
432
- }
433
-
434
- // Paginated operations: pass opts with Limit=1
435
- if (op.pagination && !op.requestBody) {
436
- const extraParams = op.queryParams.filter((p: any) => !['limit', 'before', 'after', 'order'].includes(p.name));
437
- if (extraParams.length > 0) {
438
- // Match the emitter convention: List → ListFilterOpts, others → ${method}Opts
439
- const optsType = method === 'List' ? 'ListFilterOpts' : `${method}Opts`;
440
- args.push(`${servicePackage}.${optsType}{Limit: 1}`);
478
+ args.push(generateGoPayloadStruct(payload, paramsTypeName));
441
479
  } else {
442
- args.push(`${servicePackage}.ListOpts{Limit: 1}`);
480
+ // Even with empty payload, the method signature requires the params arg
481
+ args.push(`&workos.${paramsTypeName}{}`);
443
482
  }
483
+ } else if (op.pagination || hasQueryParams) {
484
+ // Paginated or query-param operations need a params struct
485
+ args.push(`&workos.${paramsTypeName}{}`);
444
486
  }
445
487
 
446
- // Determine the service accessor on the client
447
- const serviceProp = goExportedName(resolution.service);
488
+ // Service accessor: resolve from the generated SDK's actual accessor names
489
+ const serviceProp = resolveAccessorName(resolution.service, accessorMap);
448
490
 
449
491
  lines.push(`\t// Call ${callIndex}: ${op.httpMethod.toUpperCase()} ${op.path}`);
450
492
  lines.push(`\tfmt.Fprintf(os.Stderr, "CALL_START:${callIndex}\\n")`);
451
493
 
452
- // Determine return type: paginated and GET-with-response return (result, error),
453
- // DELETE returns just error
494
+ // Determine return type: paginated returns Iterator, DELETE and void/redirect return just error
454
495
  const isDelete = op.httpMethod === 'delete';
455
- const hasResponse = !isDelete;
456
-
457
- if (hasResponse) {
458
- lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}.${method}(${args.join(', ')})`);
496
+ const isPaginated = !!op.pagination;
497
+ const isVoidResponse =
498
+ !isPaginated &&
499
+ !isDelete &&
500
+ ((op.response.kind === 'primitive' && (op.response as any).type === 'unknown') ||
501
+ (op.successResponses && op.successResponses.some((r: any) => r.statusCode >= 300 && r.statusCode < 400)));
502
+
503
+ if (isPaginated) {
504
+ // Iterator-based: call Next() once to trigger the first HTTP request
505
+ lines.push(`\titer${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
506
+ lines.push(`\titer${callIndex}.Next()`);
507
+ lines.push(`\tif err${callIndex} := iter${callIndex}.Err(); err${callIndex} != nil {`);
508
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
509
+ lines.push('\t} else {');
510
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
511
+ lines.push('\t}');
512
+ } else if (isDelete || isVoidResponse) {
513
+ lines.push(`\terr${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
459
514
  lines.push(`\tif err${callIndex} != nil {`);
460
515
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
461
516
  lines.push('\t} else {');
462
- lines.push(`\t\tjsonResult${callIndex}, _ := json.Marshal(result${callIndex})`);
463
- lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:%s\\n", string(jsonResult${callIndex}))`);
517
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
464
518
  lines.push('\t}');
465
519
  } else {
466
- lines.push(`\terr${callIndex} := client.${serviceProp}.${method}(${args.join(', ')})`);
520
+ lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
467
521
  lines.push(`\tif err${callIndex} != nil {`);
468
522
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
469
523
  lines.push('\t} else {');
470
- lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:\\n")`);
524
+ lines.push(`\t\tjsonResult${callIndex}, _ := json.Marshal(result${callIndex})`);
525
+ lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_OK:${callIndex}:%s\\n", string(jsonResult${callIndex}))`);
471
526
  lines.push('\t}');
472
527
  }
473
528
 
@@ -518,6 +573,11 @@ async function main(): Promise<void> {
518
573
  // Load manifest
519
574
  const manifest = loadManifest(sdkPath);
520
575
 
576
+ // Build accessor name map by scanning the generated workos.go for
577
+ // `func (c *Client) XxxYyy()` patterns, then matching them to manifest
578
+ // service names via case-insensitive comparison.
579
+ const accessorMap = buildAccessorMap(sdkPath);
580
+
521
581
  const baseUrl = process.env.WORKOS_BASE_URL || spec.baseUrl;
522
582
 
523
583
  // Start capture proxy
@@ -580,7 +640,11 @@ async function main(): Promise<void> {
580
640
 
581
641
  // Build planned calls for this wave, resolving methods
582
642
  const plannedCalls: PlannedCall[] = [];
583
- const waveSkipped: Array<{ op: Operation; irService: string; reason: string }> = [];
643
+ const waveSkipped: Array<{
644
+ op: Operation;
645
+ irService: string;
646
+ reason: string;
647
+ }> = [];
584
648
 
585
649
  for (const { op, irService, pathParams } of wave.calls) {
586
650
  const resolution = resolveMethod(op, irService, manifest);
@@ -624,7 +688,7 @@ async function main(): Promise<void> {
624
688
  // Generate all call blocks for this wave
625
689
  const callBlocks: string[] = [];
626
690
  for (const call of plannedCalls) {
627
- callBlocks.push(generateGoCallBlock(call.op, call.resolution, call.pathParams, spec, call.index));
691
+ callBlocks.push(generateGoCallBlock(call.op, call.resolution, call.pathParams, spec, call.index, accessorMap));
628
692
  }
629
693
 
630
694
  const imports = generateGoImports(modulePath, servicePackages, needsJson, needsServicePkg);
@@ -635,7 +699,7 @@ async function main(): Promise<void> {
635
699
  imports,
636
700
  '',
637
701
  'func main() {',
638
- `\tclient := workos.NewClient("${apiKey}", workos.WithEndpoint("http://127.0.0.1:${proxyPort}"))`,
702
+ `\tclient := workos.NewClient("${apiKey}", workos.WithBaseURL("http://127.0.0.1:${proxyPort}"))`,
639
703
  '\tctx := context.Background()',
640
704
  '',
641
705
  ...callBlocks,
@@ -651,19 +715,41 @@ async function main(): Promise<void> {
651
715
 
652
716
  // Step 1: Build (sync — no proxy needed during compilation)
653
717
  let buildError: string | null = null;
718
+
719
+ // Run go mod tidy first to resolve dependencies
654
720
  try {
655
- execSync('go build -o smoke-driver main.go', {
721
+ execSync('go mod tidy', {
656
722
  cwd: tmpDir,
657
723
  timeout: 120_000,
658
- env: { ...process.env, GOPATH: process.env.GOPATH || resolve(process.env.HOME || '~', 'go') },
724
+ env: {
725
+ ...process.env,
726
+ GOPATH: process.env.GOPATH || resolve(process.env.HOME || '~', 'go'),
727
+ },
659
728
  encoding: 'utf-8',
660
729
  stdio: ['pipe', 'pipe', 'pipe'],
661
730
  });
662
731
  } catch (err: any) {
663
732
  const stderr = typeof err.stderr === 'string' ? err.stderr : '';
664
- buildError = stderr.trim().split('\n').slice(0, 5).join(' ') || 'go build failed';
733
+ buildError = `go mod tidy failed: ${stderr.trim().split('\n').slice(0, 3).join(' ')}`;
665
734
  }
666
735
 
736
+ if (!buildError)
737
+ try {
738
+ execSync('go build -o smoke-driver main.go', {
739
+ cwd: tmpDir,
740
+ timeout: 120_000,
741
+ env: {
742
+ ...process.env,
743
+ GOPATH: process.env.GOPATH || resolve(process.env.HOME || '~', 'go'),
744
+ },
745
+ encoding: 'utf-8',
746
+ stdio: ['pipe', 'pipe', 'pipe'],
747
+ });
748
+ } catch (err: any) {
749
+ const stderr = typeof err.stderr === 'string' ? err.stderr : '';
750
+ buildError = stderr.trim().split('\n').slice(0, 5).join(' ') || 'go build failed';
751
+ }
752
+
667
753
  if (buildError) {
668
754
  // Build failure affects entire wave
669
755
  const elapsed = Date.now() - waveStart;
@@ -903,7 +989,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
903
989
  operationId: op.name,
904
990
  service,
905
991
  operationName: op.name,
906
- request: { method: op.httpMethod.toUpperCase(), path: op.path, queryParams: {}, body: null },
992
+ request: {
993
+ method: op.httpMethod.toUpperCase(),
994
+ path: op.path,
995
+ queryParams: {},
996
+ body: null,
997
+ },
907
998
  response: { status: 0, body: null },
908
999
  outcome: 'skipped',
909
1000
  error: reason,
@@ -123,7 +123,12 @@ function createProxyServer(
123
123
  }
124
124
 
125
125
  captures.push({
126
- request: { method: req.method!, path: url.pathname, queryParams, body },
126
+ request: {
127
+ method: req.method!,
128
+ path: url.pathname,
129
+ queryParams,
130
+ body,
131
+ },
127
132
  response: { status: proxyRes.statusCode!, body: resBody },
128
133
  });
129
134
 
@@ -135,7 +140,12 @@ function createProxyServer(
135
140
  proxyReq.on('error', (err) => {
136
141
  console.error('Proxy request error:', err.message);
137
142
  captures.push({
138
- request: { method: req.method!, path: url.pathname, queryParams, body },
143
+ request: {
144
+ method: req.method!,
145
+ path: url.pathname,
146
+ queryParams,
147
+ body,
148
+ },
139
149
  response: { status: 502, body: { error: err.message } },
140
150
  });
141
151
  res.writeHead(502);
@@ -499,7 +509,11 @@ async function main(): Promise<void> {
499
509
 
500
510
  // Build planned calls for this wave, resolving methods
501
511
  const plannedCalls: PlannedCall[] = [];
502
- const waveSkipped: Array<{ op: Operation; irService: string; reason: string }> = [];
512
+ const waveSkipped: Array<{
513
+ op: Operation;
514
+ irService: string;
515
+ reason: string;
516
+ }> = [];
503
517
 
504
518
  for (const { op, irService, pathParams } of wave.calls) {
505
519
  const resolution = resolveMethod(op, irService, manifest);
@@ -754,7 +768,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
754
768
  operationId: op.name,
755
769
  service,
756
770
  operationName: op.name,
757
- request: { method: op.httpMethod.toUpperCase(), path: op.path, queryParams: {}, body: null },
771
+ request: {
772
+ method: op.httpMethod.toUpperCase(),
773
+ path: op.path,
774
+ queryParams: {},
775
+ body: null,
776
+ },
758
777
  response: { status: 0, body: null },
759
778
  outcome: 'skipped',
760
779
  error: reason,
package/smoke/sdk-node.ts CHANGED
@@ -52,7 +52,10 @@ interface CapturedResponse {
52
52
  // HTTP Interception
53
53
  // ---------------------------------------------------------------------------
54
54
 
55
- let currentCapture: { request: CapturedRequest; response: CapturedResponse } | null = null;
55
+ let currentCapture: {
56
+ request: CapturedRequest;
57
+ response: CapturedResponse;
58
+ } | null = null;
56
59
  const originalFetch = globalThis.fetch;
57
60
 
58
61
  function interceptFetch(): void {
@@ -259,7 +262,11 @@ async function main(): Promise<void> {
259
262
  const groups = planOperations(spec);
260
263
  const ids = new IdRegistry();
261
264
  const exchanges: CapturedExchange[] = [];
262
- const createdEntities: Array<{ service: string; id: string; deleteFn?: () => Promise<void> }> = [];
265
+ const createdEntities: Array<{
266
+ service: string;
267
+ id: string;
268
+ deleteFn?: () => Promise<void>;
269
+ }> = [];
263
270
  const delayMs = Number(process.env.SMOKE_DELAY_MS) || 200;
264
271
 
265
272
  let successCount = 0;
@@ -452,7 +459,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
452
459
  operationId: op.name,
453
460
  service,
454
461
  operationName: op.name,
455
- request: { method: op.httpMethod.toUpperCase(), path: op.path, queryParams: {}, body: null },
462
+ request: {
463
+ method: op.httpMethod.toUpperCase(),
464
+ path: op.path,
465
+ queryParams: {},
466
+ body: null,
467
+ },
456
468
  response: { status: 0, body: null },
457
469
  outcome: 'skipped',
458
470
  error: reason,
package/smoke/sdk-php.ts CHANGED
@@ -184,8 +184,8 @@ function loadManifest(sdkPath: string): Map<string, ManifestEntry> | null {
184
184
  // ---------------------------------------------------------------------------
185
185
 
186
186
  interface MethodResolution {
187
- className: string;
188
- method: string;
187
+ service: string; // camelCase accessor on client (e.g., "organizations")
188
+ method: string; // camelCase method name (e.g., "get")
189
189
  tier: ExchangeProvenance['resolutionTier'];
190
190
  confidence: number;
191
191
  }
@@ -200,14 +200,12 @@ function resolveMethod(
200
200
  if (manifest) {
201
201
  const entry = manifest.get(httpKey);
202
202
  if (entry) {
203
- const className = entry.service.charAt(0).toUpperCase() + entry.service.slice(1);
204
- return { className, method: entry.sdkMethod, tier: 'manifest', confidence: 1.0 };
203
+ return { service: entry.service, method: entry.sdkMethod, tier: 'manifest', confidence: 1.0 };
205
204
  }
206
205
  }
207
206
 
208
207
  const sdkProp = SERVICE_PROPERTY_MAP[irService] || toCamelCase(irService);
209
- const className = sdkProp.charAt(0).toUpperCase() + sdkProp.slice(1);
210
- return { className, method: toCamelCase(op.name), tier: 'exact', confidence: 0.8 };
208
+ return { service: sdkProp, method: toCamelCase(op.name), tier: 'exact', confidence: 0.8 };
211
209
  }
212
210
 
213
211
  // ---------------------------------------------------------------------------
@@ -278,9 +276,14 @@ function buildBatchedPhpScript(
278
276
  }
279
277
  lines.push('');
280
278
 
281
- // Configure SDK
282
- lines.push(`${namespace}\\Client::setApiKey('${apiKey}');`);
283
- lines.push(`${namespace}\\Client::setBaseUrl('http://127.0.0.1:${proxyPort}');`);
279
+ // Configure SDK — generated SDK uses instance-based client with Guzzle handler
280
+ lines.push(`use GuzzleHttp\\HandlerStack;`);
281
+ lines.push(`use GuzzleHttp\\Handler\\CurlHandler;`);
282
+ lines.push('');
283
+ lines.push(`$client = new ${namespace}\\${namespace}(`);
284
+ lines.push(` apiKey: '${escapePhpString(apiKey)}',`);
285
+ lines.push(` baseUrl: 'http://127.0.0.1:${proxyPort}',`);
286
+ lines.push(');');
284
287
  lines.push('');
285
288
 
286
289
  for (const call of calls) {
@@ -289,7 +292,8 @@ function buildBatchedPhpScript(
289
292
  // Marker: start
290
293
  lines.push(`fwrite(STDERR, "OAGEN_CALL_START:${index}\\n");`);
291
294
 
292
- // Build arguments
295
+ // Build arguments — generated PHP SDK takes positional path params,
296
+ // then named keyword args for body fields and query params
293
297
  const phpArgs: string[] = [];
294
298
 
295
299
  for (const p of op.pathParams) {
@@ -299,33 +303,31 @@ function buildBatchedPhpScript(
299
303
 
300
304
  if (op.requestBody) {
301
305
  const payload = generatePayload(op, spec);
302
- if (payload && Object.keys(payload).length > 0) {
303
- phpArgs.push(phpArrayLiteral(payload));
304
- } else {
305
- phpArgs.push('[]');
306
+ if (payload && typeof payload === 'object') {
307
+ // Pass as named arguments (the generated SDK uses promoted properties)
308
+ for (const [key, value] of Object.entries(payload)) {
309
+ phpArgs.push(`${toCamelCase(key)}: ${phpArrayLiteral(value)}`);
310
+ }
306
311
  }
307
312
  }
308
313
 
309
314
  if (!op.requestBody && op.queryParams.some((p) => p.required)) {
310
315
  const queryOpts = generateQueryParams(op, spec);
311
- if (Object.keys(queryOpts).length > 0) {
312
- phpArgs.push(phpArrayLiteral(queryOpts));
316
+ for (const [key, value] of Object.entries(queryOpts)) {
317
+ phpArgs.push(`${toCamelCase(key)}: ${phpArrayLiteral(value)}`);
313
318
  }
314
319
  }
315
320
 
316
- if (op.pagination && phpArgs.length === 0) {
317
- phpArgs.push("['limit' => 1]");
318
- } else if (op.pagination && !op.requestBody) {
319
- const last = phpArgs[phpArgs.length - 1];
320
- if (last && last.startsWith('[')) {
321
- phpArgs[phpArgs.length - 1] = last.replace(/\]$/, ", 'limit' => 1]");
322
- } else {
323
- phpArgs.push("['limit' => 1]");
321
+ if (op.pagination) {
322
+ if (!phpArgs.some((a) => a.startsWith('limit:'))) {
323
+ phpArgs.push('limit: 1');
324
324
  }
325
325
  }
326
326
 
327
+ // The generated SDK uses $client->resource()->method(...) pattern
328
+ const serviceAccessor = resolution.service;
327
329
  lines.push('try {');
328
- lines.push(` $result = ${namespace}\\${resolution.className}::${resolution.method}(${phpArgs.join(', ')});`);
330
+ lines.push(` $result = $client->${serviceAccessor}()->${resolution.method}(${phpArgs.join(', ')});`);
329
331
  lines.push(` fwrite(STDERR, "OAGEN_CALL_OK:${index}\\n");`);
330
332
  lines.push('} catch (\\Throwable $e) {');
331
333
  lines.push(` fwrite(STDERR, "OAGEN_CALL_ERROR:${index}:" . $e->getMessage() . "\\n");`);
@@ -669,7 +671,7 @@ function buildExchange(
669
671
  provenance: {
670
672
  resolutionTier: resolution.tier,
671
673
  resolutionConfidence: resolution.confidence,
672
- sdkMethodName: `${resolution.className}::${resolution.method}`,
674
+ sdkMethodName: `${resolution.service}->${resolution.method}`,
673
675
  captureIndex: 0,
674
676
  totalCaptures: 1,
675
677
  },
@@ -284,7 +284,10 @@ function buildBatchedPythonScript(
284
284
  calls: PlannedCall[],
285
285
  spec: any,
286
286
  ): string {
287
- const srcPath = resolve(sdkPath, 'src');
287
+ // Use src/ subdirectory if it exists, otherwise use the SDK root directly.
288
+ // Generated SDKs use a flat layout (workos/ at root), while some hand-written
289
+ // SDKs nest under src/.
290
+ const srcPath = existsSync(resolve(sdkPath, 'src')) ? resolve(sdkPath, 'src') : resolve(sdkPath);
288
291
  const lines: string[] = [];
289
292
 
290
293
  // Preamble -- loaded once
@@ -504,7 +507,7 @@ async function main(): Promise<void> {
504
507
  const child = spawn(python3Path, [scriptPath], {
505
508
  env: {
506
509
  ...process.env,
507
- PYTHONPATH: resolve(sdkPath, 'src'),
510
+ PYTHONPATH: existsSync(resolve(sdkPath, 'src')) ? resolve(sdkPath, 'src') : resolve(sdkPath),
508
511
  PYTHONDONTWRITEBYTECODE: '1',
509
512
  },
510
513
  stdio: ['pipe', 'pipe', 'pipe'],
package/smoke/sdk-ruby.ts CHANGED
@@ -160,7 +160,12 @@ function startProxy(
160
160
  }
161
161
  }
162
162
 
163
- const capturedReq: CapturedRequest = { method, path, queryParams, body: reqBody };
163
+ const capturedReq: CapturedRequest = {
164
+ method,
165
+ path,
166
+ queryParams,
167
+ body: reqBody,
168
+ };
164
169
 
165
170
  // Forward to real API
166
171
  const forwardHeaders: Record<string, string> = {
@@ -420,7 +425,11 @@ async function main(): Promise<void> {
420
425
 
421
426
  // Build planned calls for this wave, resolving methods
422
427
  const plannedCalls: PlannedCall[] = [];
423
- const waveSkipped: Array<{ op: Operation; irService: string; reason: string }> = [];
428
+ const waveSkipped: Array<{
429
+ op: Operation;
430
+ irService: string;
431
+ reason: string;
432
+ }> = [];
424
433
 
425
434
  for (const { op, irService, pathParams } of wave.calls) {
426
435
  const resolution = resolveMethod(op, irService, manifest);
@@ -678,7 +687,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
678
687
  operationId: op.name,
679
688
  service,
680
689
  operationName: op.name,
681
- request: { method: op.httpMethod.toUpperCase(), path: op.path, queryParams: {}, body: null },
690
+ request: {
691
+ method: op.httpMethod.toUpperCase(),
692
+ path: op.path,
693
+ queryParams: {},
694
+ body: null,
695
+ },
682
696
  response: { status: 0, body: null },
683
697
  outcome: 'skipped',
684
698
  error: reason,
package/smoke/sdk-rust.ts CHANGED
@@ -452,7 +452,11 @@ serde_json = "1"
452
452
 
453
453
  // Build planned calls for this wave, resolving methods
454
454
  const plannedCalls: PlannedCall[] = [];
455
- const waveSkipped: Array<{ op: Operation; irService: string; reason: string }> = [];
455
+ const waveSkipped: Array<{
456
+ op: Operation;
457
+ irService: string;
458
+ reason: string;
459
+ }> = [];
456
460
 
457
461
  for (const { op, irService, pathParams } of wave.calls) {
458
462
  const resolution = resolveMethod(op, irService, manifest);
@@ -534,7 +538,11 @@ serde_json = "1"
534
538
  await new Promise<void>((resolvePromise, rejectPromise) => {
535
539
  const child = spawn(join(tmpDir, 'target', 'debug', 'smoke-driver'), [], {
536
540
  cwd: tmpDir,
537
- env: { ...process.env, WORKOS_API_KEY: apiKey, WORKOS_BASE_URL: `http://localhost:${proxy.port}` },
541
+ env: {
542
+ ...process.env,
543
+ WORKOS_API_KEY: apiKey,
544
+ WORKOS_BASE_URL: `http://localhost:${proxy.port}`,
545
+ },
538
546
  stdio: ['pipe', 'pipe', 'pipe'],
539
547
  });
540
548
 
@@ -729,7 +737,12 @@ function makeSkippedExchange(op: Operation, service: string, reason: string): Ca
729
737
  operationId: op.name,
730
738
  service,
731
739
  operationName: op.name,
732
- request: { method: op.httpMethod.toUpperCase(), path: op.path, queryParams: {}, body: null },
740
+ request: {
741
+ method: op.httpMethod.toUpperCase(),
742
+ path: op.path,
743
+ queryParams: {},
744
+ body: null,
745
+ },
733
746
  response: { status: 0, body: null },
734
747
  outcome: 'skipped',
735
748
  error: reason,