@workos/oagen-emitters 0.2.1 → 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 (103) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +8 -0
  4. package/README.md +129 -0
  5. package/dist/index.d.mts +10 -1
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +11893 -3226
  8. package/dist/index.mjs.map +1 -1
  9. package/docs/sdk-architecture/go.md +338 -0
  10. package/docs/sdk-architecture/php.md +315 -0
  11. package/docs/sdk-architecture/python.md +511 -0
  12. package/oagen.config.ts +298 -2
  13. package/package.json +9 -5
  14. package/scripts/generate-php.js +13 -0
  15. package/scripts/git-push-with-published-oagen.sh +21 -0
  16. package/smoke/sdk-go.ts +116 -42
  17. package/smoke/sdk-php.ts +28 -26
  18. package/smoke/sdk-python.ts +5 -2
  19. package/src/go/client.ts +141 -0
  20. package/src/go/enums.ts +196 -0
  21. package/src/go/fixtures.ts +212 -0
  22. package/src/go/index.ts +81 -0
  23. package/src/go/manifest.ts +36 -0
  24. package/src/go/models.ts +254 -0
  25. package/src/go/naming.ts +191 -0
  26. package/src/go/resources.ts +827 -0
  27. package/src/go/tests.ts +751 -0
  28. package/src/go/type-map.ts +82 -0
  29. package/src/go/wrappers.ts +261 -0
  30. package/src/index.ts +3 -0
  31. package/src/node/client.ts +78 -115
  32. package/src/node/enums.ts +9 -0
  33. package/src/node/errors.ts +37 -232
  34. package/src/node/field-plan.ts +726 -0
  35. package/src/node/fixtures.ts +9 -1
  36. package/src/node/index.ts +2 -9
  37. package/src/node/models.ts +178 -21
  38. package/src/node/naming.ts +49 -111
  39. package/src/node/resources.ts +374 -364
  40. package/src/node/sdk-errors.ts +41 -0
  41. package/src/node/tests.ts +32 -12
  42. package/src/node/type-map.ts +4 -2
  43. package/src/node/utils.ts +13 -71
  44. package/src/node/wrappers.ts +151 -0
  45. package/src/php/client.ts +171 -0
  46. package/src/php/enums.ts +67 -0
  47. package/src/php/errors.ts +9 -0
  48. package/src/php/fixtures.ts +181 -0
  49. package/src/php/index.ts +96 -0
  50. package/src/php/manifest.ts +36 -0
  51. package/src/php/models.ts +310 -0
  52. package/src/php/naming.ts +298 -0
  53. package/src/php/resources.ts +561 -0
  54. package/src/php/tests.ts +533 -0
  55. package/src/php/type-map.ts +90 -0
  56. package/src/php/utils.ts +18 -0
  57. package/src/php/wrappers.ts +151 -0
  58. package/src/python/client.ts +337 -0
  59. package/src/python/enums.ts +313 -0
  60. package/src/python/fixtures.ts +196 -0
  61. package/src/python/index.ts +95 -0
  62. package/src/python/manifest.ts +38 -0
  63. package/src/python/models.ts +688 -0
  64. package/src/python/naming.ts +209 -0
  65. package/src/python/resources.ts +1322 -0
  66. package/src/python/tests.ts +1335 -0
  67. package/src/python/type-map.ts +93 -0
  68. package/src/python/wrappers.ts +191 -0
  69. package/src/shared/model-utils.ts +255 -0
  70. package/src/shared/naming-utils.ts +107 -0
  71. package/src/shared/non-spec-services.ts +54 -0
  72. package/src/shared/resolved-ops.ts +109 -0
  73. package/src/shared/wrapper-utils.ts +59 -0
  74. package/test/go/client.test.ts +92 -0
  75. package/test/go/enums.test.ts +132 -0
  76. package/test/go/errors.test.ts +9 -0
  77. package/test/go/models.test.ts +265 -0
  78. package/test/go/resources.test.ts +408 -0
  79. package/test/go/tests.test.ts +143 -0
  80. package/test/node/client.test.ts +18 -12
  81. package/test/node/enums.test.ts +2 -0
  82. package/test/node/errors.test.ts +2 -41
  83. package/test/node/models.test.ts +2 -0
  84. package/test/node/naming.test.ts +23 -0
  85. package/test/node/resources.test.ts +99 -69
  86. package/test/node/serializers.test.ts +3 -1
  87. package/test/node/type-map.test.ts +11 -0
  88. package/test/php/client.test.ts +94 -0
  89. package/test/php/enums.test.ts +173 -0
  90. package/test/php/errors.test.ts +9 -0
  91. package/test/php/models.test.ts +497 -0
  92. package/test/php/resources.test.ts +644 -0
  93. package/test/php/tests.test.ts +118 -0
  94. package/test/python/client.test.ts +200 -0
  95. package/test/python/enums.test.ts +228 -0
  96. package/test/python/errors.test.ts +16 -0
  97. package/test/python/manifest.test.ts +74 -0
  98. package/test/python/models.test.ts +716 -0
  99. package/test/python/resources.test.ts +617 -0
  100. package/test/python/tests.test.ts +202 -0
  101. package/src/node/common.ts +0 -273
  102. package/src/node/config.ts +0 -71
  103. package/src/node/serializers.ts +0 -746
package/oagen.config.ts CHANGED
@@ -1,6 +1,9 @@
1
- import type { OagenConfig } from '@workos/oagen';
1
+ import type { OagenConfig, OperationHint } from '@workos/oagen';
2
2
  import { toCamelCase } from '@workos/oagen';
3
3
  import { nodeEmitter } from './src/node/index.js';
4
+ import { pythonEmitter } from './src/python/index.js';
5
+ import { phpEmitter } from './src/php/index.js';
6
+ import { goEmitter } from './src/go/index.js';
4
7
  import { nodeExtractor } from './src/compat/extractors/node.js';
5
8
  import { rubyExtractor } from './src/compat/extractors/ruby.js';
6
9
  import { pythonExtractor } from './src/compat/extractors/python.js';
@@ -21,8 +24,282 @@ function nestjsOperationIdTransform(id: string): string {
21
24
  return idx !== -1 ? toCamelCase(stripped.slice(idx + 1)) : toCamelCase(stripped);
22
25
  }
23
26
 
27
+ // ---------------------------------------------------------------------------
28
+ // Operation hints — per-operation overrides for the operation resolver.
29
+ // Keyed by "METHOD /path". Only operations that need overrides are listed;
30
+ // the algorithm handles the rest.
31
+ // ---------------------------------------------------------------------------
32
+ const operationHints: Record<string, OperationHint> = {
33
+ // ── Radar ────────────────────────────────────────────────────────────────
34
+ 'POST /radar/lists/{type}/{action}': { name: 'add_list_entry' },
35
+ 'DELETE /radar/lists/{type}/{action}': { name: 'remove_list_entry' },
36
+
37
+ // ── SSO ──────────────────────────────────────────────────────────────────
38
+ 'GET /sso/authorize': {
39
+ name: 'get_authorization_url',
40
+ defaults: { response_type: 'code' },
41
+ inferFromClient: ['client_id'],
42
+ },
43
+ 'GET /sso/logout': { name: 'get_logout_url' },
44
+ 'GET /sso/profile': { name: 'get_profile' },
45
+ 'POST /sso/token': {
46
+ name: 'get_profile_and_token',
47
+ defaults: { grant_type: 'authorization_code' },
48
+ inferFromClient: ['client_id', 'client_secret'],
49
+ },
50
+
51
+ // ── SSO / JWKS (mounted on UserManagement via mountRules) ────────────────
52
+ 'GET /sso/jwks/{clientId}': { name: 'get_jwks' },
53
+
54
+ // ── User Management — auth ──────────────────────────────────────────────
55
+ 'GET /user_management/authorize': {
56
+ name: 'get_authorization_url',
57
+ defaults: { response_type: 'code' },
58
+ inferFromClient: ['client_id'],
59
+ },
60
+ 'GET /user_management/sessions/logout': { name: 'get_logout_url' },
61
+
62
+ // ── User Management — org membership actions ────────────────────────────
63
+ 'PUT /user_management/organization_memberships/{id}/deactivate': {
64
+ name: 'deactivate_organization_membership',
65
+ },
66
+ 'PUT /user_management/organization_memberships/{id}/reactivate': {
67
+ name: 'reactivate_organization_membership',
68
+ },
69
+
70
+ // ── Admin Portal ────────────────────────────────────────────────────────
71
+ 'POST /portal/generate_link': { name: 'generate_link' },
72
+
73
+ // ── Feature Flags — disambiguate co-mounted list operations ─────────────
74
+ 'GET /organizations/{organizationId}/feature-flags': { name: 'list_organization_feature_flags' },
75
+ 'GET /user_management/users/{userId}/feature-flags': { name: 'list_user_feature_flags' },
76
+
77
+ // ── External ID lookups (not derivable from path) ──────────────────────
78
+ 'GET /organizations/external_id/{external_id}': { name: 'get_organization_by_external_id' },
79
+ 'GET /user_management/users/external_id/{external_id}': { name: 'get_user_by_external_id' },
80
+
81
+ // ── Authorization — environment-scoped roles ─────────────────────────────
82
+ 'GET /authorization/roles': { name: 'list_environment_roles' },
83
+ 'POST /authorization/roles': { name: 'create_environment_role' },
84
+ 'GET /authorization/roles/{slug}': { name: 'get_environment_role' },
85
+ 'PATCH /authorization/roles/{slug}': { name: 'update_environment_role' },
86
+ 'PUT /authorization/roles/{slug}/permissions': {
87
+ name: 'set_environment_role_permissions',
88
+ },
89
+ 'POST /authorization/roles/{slug}/permissions': {
90
+ name: 'add_environment_role_permission',
91
+ },
92
+
93
+ // ── Authorization — singularized/shortened names ────────────────────────
94
+ 'POST /authorization/permissions': { name: 'create_permission' },
95
+ 'POST /authorization/resources': { name: 'create_resource' },
96
+ 'POST /authorization/organization_memberships/{organization_membership_id}/check': {
97
+ name: 'check',
98
+ },
99
+ 'POST /authorization/organization_memberships/{organization_membership_id}/role_assignments': {
100
+ name: 'assign_role',
101
+ },
102
+ 'DELETE /authorization/organization_memberships/{organization_membership_id}/role_assignments': {
103
+ name: 'remove_role',
104
+ },
105
+ 'POST /authorization/organizations/{organizationId}/roles': {
106
+ name: 'create_organization_role',
107
+ },
108
+
109
+ // ── Authorization — env-scoped resource memberships ────────────────────
110
+ 'GET /authorization/resources/{resource_id}/organization_memberships': { name: 'list_memberships_for_resource' },
111
+
112
+ // ── User Management — singularized/shortened names ─────────────────────
113
+ 'POST /user_management/users': { name: 'create_user' },
114
+ 'POST /user_management/organization_memberships': {
115
+ name: 'create_organization_membership',
116
+ },
117
+ 'POST /user_management/invitations': { name: 'send_invitation' },
118
+ 'GET /user_management/invitations/by_token/{token}': {
119
+ name: 'find_invitation_by_token',
120
+ },
121
+ 'POST /user_management/users/{id}/email_verification/send': {
122
+ name: 'send_verification_email',
123
+ },
124
+ 'POST /user_management/users/{id}/email_verification/confirm': {
125
+ name: 'verify_email',
126
+ },
127
+ 'POST /user_management/password_reset': { name: 'reset_password' },
128
+ 'POST /user_management/password_reset/confirm': {
129
+ name: 'confirm_password_reset',
130
+ },
131
+ 'GET /user_management/users/{id}/sessions': { name: 'list_sessions' },
132
+ 'GET /user_management/users/{id}/identities': { name: 'get_user_identities' },
133
+ 'POST /user_management/cors_origins': { name: 'create_cors_origin' },
134
+ 'POST /user_management/redirect_uris': { name: 'create_redirect_uri' },
135
+
136
+ // ── Organizations — singularized names ─────────────────────────────────
137
+ 'POST /organizations': { name: 'create_organization' },
138
+
139
+ // ── Directory Sync — shortened names ───────────────────────────────────
140
+ 'GET /directory_groups': { name: 'list_groups' },
141
+ 'GET /directory_groups/{id}': { name: 'get_group' },
142
+ 'GET /directory_users': { name: 'list_users' },
143
+ 'GET /directory_users/{id}': { name: 'get_user' },
144
+
145
+ // ── Audit Logs — singularized names ────────────────────────────────────
146
+ 'POST /audit_logs/events': { name: 'create_event' },
147
+ 'POST /audit_logs/exports': { name: 'create_export' },
148
+ 'POST /audit_logs/actions/{actionName}/schemas': { name: 'create_schema' },
149
+
150
+ // ── Feature Flags — match SDK conventions ──────────────────────────────
151
+ 'POST /feature-flags/{slug}/targets/{resourceId}': { name: 'add_flag_target' },
152
+ 'DELETE /feature-flags/{slug}/targets/{resourceId}': {
153
+ name: 'remove_flag_target',
154
+ },
155
+
156
+ // ── Organizations — audit logs retention (mounted on AuditLogs) ─────────
157
+ 'GET /organizations/{id}/audit_logs_retention': { mountOn: 'AuditLogs' },
158
+ 'PUT /organizations/{id}/audit_logs_retention': { mountOn: 'AuditLogs' },
159
+
160
+ // ── Union split: POST /user_management/authenticate (8 variants) ────────
161
+ 'POST /user_management/authenticate': {
162
+ split: [
163
+ {
164
+ name: 'authenticate_with_password',
165
+ targetVariant: 'PasswordSessionAuthenticateRequest',
166
+ defaults: { grant_type: 'password' },
167
+ inferFromClient: ['client_id', 'client_secret'],
168
+ exposedParams: ['email', 'password', 'invitation_token'],
169
+ },
170
+ {
171
+ name: 'authenticate_with_code',
172
+ targetVariant: 'CodeSessionAuthenticateRequest',
173
+ defaults: { grant_type: 'authorization_code' },
174
+ inferFromClient: ['client_id', 'client_secret'],
175
+ exposedParams: ['code'],
176
+ },
177
+ {
178
+ name: 'authenticate_with_refresh_token',
179
+ targetVariant: 'RefreshTokenSessionAuthenticateRequest',
180
+ defaults: { grant_type: 'refresh_token' },
181
+ inferFromClient: ['client_id', 'client_secret'],
182
+ exposedParams: ['refresh_token', 'organization_id'],
183
+ },
184
+ {
185
+ name: 'authenticate_with_magic_auth',
186
+ targetVariant: 'MagicAuthSessionAuthenticateRequest',
187
+ defaults: { grant_type: 'urn:workos:oauth:grant-type:magic-auth:code' },
188
+ inferFromClient: ['client_id', 'client_secret'],
189
+ exposedParams: ['code', 'email', 'invitation_token'],
190
+ },
191
+ {
192
+ name: 'authenticate_with_email_verification',
193
+ targetVariant: 'EmailVerificationSessionAuthenticateRequest',
194
+ defaults: { grant_type: 'urn:workos:oauth:grant-type:email-verification:code' },
195
+ inferFromClient: ['client_id', 'client_secret'],
196
+ exposedParams: ['code', 'pending_authentication_token'],
197
+ },
198
+ {
199
+ name: 'authenticate_with_totp',
200
+ targetVariant: 'TotpSessionAuthenticateRequest',
201
+ defaults: { grant_type: 'urn:workos:oauth:grant-type:mfa-totp' },
202
+ inferFromClient: ['client_id', 'client_secret'],
203
+ exposedParams: ['code', 'pending_authentication_token', 'authentication_challenge_id'],
204
+ },
205
+ {
206
+ name: 'authenticate_with_organization_selection',
207
+ targetVariant: 'OrganizationSelectionSessionAuthenticateRequest',
208
+ defaults: { grant_type: 'urn:workos:oauth:grant-type:organization-selection' },
209
+ inferFromClient: ['client_id', 'client_secret'],
210
+ exposedParams: ['pending_authentication_token', 'organization_id'],
211
+ },
212
+ {
213
+ name: 'authenticate_with_device_code',
214
+ targetVariant: 'DeviceCodeSessionAuthenticateRequest',
215
+ defaults: { grant_type: 'urn:ietf:params:oauth:grant-type:device_code' },
216
+ inferFromClient: ['client_id'],
217
+ exposedParams: ['device_code'],
218
+ },
219
+ ],
220
+ },
221
+
222
+ // ── Union split: POST /connect/applications (2 variants) ────────────────
223
+ 'POST /connect/applications': {
224
+ split: [
225
+ {
226
+ name: 'create_oauth_application',
227
+ targetVariant: 'CreateOAuthApplication',
228
+ defaults: { application_type: 'oauth' },
229
+ exposedParams: [
230
+ 'name',
231
+ 'is_first_party',
232
+ 'description',
233
+ 'scopes',
234
+ 'redirect_uris',
235
+ 'uses_pkce',
236
+ 'organization_id',
237
+ ],
238
+ },
239
+ {
240
+ name: 'create_m2m_application',
241
+ targetVariant: 'CreateM2MApplication',
242
+ defaults: { application_type: 'm2m' },
243
+ exposedParams: ['name', 'organization_id', 'description', 'scopes'],
244
+ },
245
+ ],
246
+ },
247
+ };
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Mount rules — service-level remounting. Maps IR service name → target
251
+ // service/namespace (PascalCase). All operations in the source service are
252
+ // mounted on the target unless overridden per-operation in operationHints.
253
+ // ---------------------------------------------------------------------------
254
+ const mountRules: Record<string, string> = {
255
+ // MFA sub-services → MultiFactorAuth
256
+ MultiFactorAuthChallenges: 'MultiFactorAuth',
257
+
258
+ // RBAC permissions → Authorization
259
+ Permissions: 'Authorization',
260
+
261
+ // Connect sub-services → Connect
262
+ WorkosConnect: 'Connect',
263
+ Applications: 'Connect',
264
+ ApplicationClientSecrets: 'Connect',
265
+
266
+ // SSO connections → SSO
267
+ Connections: 'SSO',
268
+
269
+ // Directory Sync sub-services → DirectorySync
270
+ Directories: 'DirectorySync',
271
+ DirectoryGroups: 'DirectorySync',
272
+ DirectoryUsers: 'DirectorySync',
273
+
274
+ // Feature flag sub-services → FeatureFlags
275
+ FeatureFlagsTargets: 'FeatureFlags',
276
+ OrganizationsFeatureFlags: 'FeatureFlags',
277
+ UserManagementUsersFeatureFlags: 'FeatureFlags',
278
+
279
+ // Org API keys → ApiKeys
280
+ OrganizationsApiKeys: 'ApiKeys',
281
+
282
+ // User Management sub-services → UserManagement
283
+ UserManagementSessionTokens: 'UserManagement',
284
+ UserManagementAuthentication: 'UserManagement',
285
+ UserManagementCorsOrigins: 'UserManagement',
286
+ UserManagementUsers: 'UserManagement',
287
+ UserManagementInvitations: 'UserManagement',
288
+ UserManagementJWTTemplate: 'UserManagement',
289
+ UserManagementMagicAuth: 'UserManagement',
290
+ UserManagementOrganizationMembership: 'UserManagement',
291
+ UserManagementRedirectUris: 'UserManagement',
292
+ UserManagementUsersAuthorizedApplications: 'UserManagement',
293
+
294
+ // Pipes / Data Providers → Pipes
295
+ UserManagementDataProviders: 'Pipes',
296
+
297
+ // User Management MFA → MultiFactorAuth
298
+ UserManagementMultiFactorAuthentication: 'MultiFactorAuth',
299
+ };
300
+
24
301
  const config: OagenConfig = {
25
- emitters: [nodeEmitter],
302
+ emitters: [nodeEmitter, pythonEmitter, phpEmitter, goEmitter],
26
303
  extractors: [
27
304
  nodeExtractor,
28
305
  rubyExtractor,
@@ -47,5 +324,24 @@ const config: OagenConfig = {
47
324
  },
48
325
  docUrl: 'https://workos.com/docs',
49
326
  operationIdTransform: nestjsOperationIdTransform,
327
+ schemaNameTransform: (name: string) => {
328
+ // Explicit renames for Dto models that collide with response models
329
+ const COLLISION_RENAMES: Record<string, string> = {
330
+ OrganizationDto: 'OrganizationInput',
331
+ RedirectUriDto: 'RedirectUriInput',
332
+ // Generic list-derived names that need domain-specific identifiers
333
+ ListData: 'Role',
334
+ ListModel: 'RoleList',
335
+ // Double-List naming artifact
336
+ EventListListMetadata: 'EventListMetadata',
337
+ };
338
+ if (COLLISION_RENAMES[name]) return COLLISION_RENAMES[name];
339
+ return name
340
+ .replace(/Dto/g, '')
341
+ .replace(/DTO/g, '')
342
+ .replace(/^Urn(?:IetfParams|Workos)O[Aa]uthGrantType/, '');
343
+ },
344
+ operationHints,
345
+ mountRules,
50
346
  };
51
347
  export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -28,6 +28,10 @@
28
28
  "test": "vitest run",
29
29
  "test:watch": "vitest",
30
30
  "typecheck": "tsc --noEmit",
31
+ "git:push": "sh ./scripts/git-push-with-published-oagen.sh",
32
+ "oagen:build:local": "npm --prefix ../oagen run build",
33
+ "oagen:use:local": "npm run oagen:build:local && npm link ../oagen",
34
+ "oagen:use:published": "sh -c 'npm unlink @workos/oagen >/dev/null 2>&1 || true; npm install --ignore-scripts @workos/oagen@^0.4.0'",
31
35
  "prepare": "husky",
32
36
  "sdk:generate:dotnet": "oagen generate --lang dotnet --output ./sdk-dotnet --namespace workos --api-surface ./sdk-dotnet-surface.json",
33
37
  "sdk:verify:dotnet": "oagen verify --lang dotnet --output ./sdk-dotnet --api-surface ./sdk-dotnet-surface.json",
@@ -44,7 +48,7 @@
44
48
  "sdk:generate:node": "oagen generate --lang node --output ./sdk-node --namespace workos --api-surface ./sdk-node-surface.json",
45
49
  "sdk:verify:node": "oagen verify --lang node --output ./sdk-node --api-surface ./sdk-node-surface.json",
46
50
  "sdk:extract:node": "oagen extract --lang node --sdk-path ../backend/workos-node --output ./sdk-node-surface.json",
47
- "sdk:generate:php": "oagen generate --lang php --output ./sdk-php --namespace workos --api-surface ./sdk-php-surface.json",
51
+ "sdk:generate:php": "node scripts/generate-php.js",
48
52
  "sdk:verify:php": "oagen verify --lang php --output ./sdk-php --api-surface ./sdk-php-surface.json",
49
53
  "sdk:extract:php": "oagen extract --lang php --sdk-path ../backend/workos-php --output ./sdk-php-surface.json",
50
54
  "sdk:generate:python": "oagen generate --lang python --output ./sdk-python --namespace workos --api-surface ./sdk-python-surface.json",
@@ -57,9 +61,6 @@
57
61
  "sdk:verify:rust": "oagen verify --lang rust --output ./sdk-rust --api-surface ./sdk-rust-surface.json",
58
62
  "sdk:extract:rust": "oagen extract --lang rust --sdk-path ../backend/workos-rust --output ./sdk-rust-surface.json"
59
63
  },
60
- "dependencies": {
61
- "@workos/oagen": "^0.3.0"
62
- },
63
64
  "devDependencies": {
64
65
  "@commitlint/cli": "^20.5.0",
65
66
  "@commitlint/config-conventional": "^20.5.0",
@@ -75,5 +76,8 @@
75
76
  },
76
77
  "engines": {
77
78
  "node": ">=24.10.0"
79
+ },
80
+ "dependencies": {
81
+ "@workos/oagen": "^0.5.0"
78
82
  }
79
83
  }
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PHP SDK generation wrapper.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+
7
+ // Build oagen command with all passed arguments
8
+ const extraArgs = process.argv.slice(2).join(' ');
9
+ const baseCmd = 'oagen generate --lang php --output ./sdk-php --namespace WorkOS --api-surface ./sdk-php-surface.json';
10
+ const fullCmd = extraArgs ? `${baseCmd} ${extraArgs}` : baseCmd;
11
+
12
+ // Run oagen generate
13
+ execSync(fullCmd, { stdio: 'inherit' });
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ repo_root="$(git rev-parse --show-toplevel)"
5
+ cd "$repo_root"
6
+
7
+ restore_local() {
8
+ echo "push wrapper: restoring local ../oagen link"
9
+ npm run oagen:use:local
10
+ }
11
+
12
+ cleanup() {
13
+ restore_local
14
+ }
15
+
16
+ trap cleanup EXIT INT TERM HUP
17
+
18
+ echo "push wrapper: switching @workos/oagen to the published package"
19
+ npm run oagen:use:published
20
+
21
+ git push "$@"
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
  // ---------------------------------------------------------------------------
@@ -345,15 +382,16 @@ function detectModulePath(sdkPath: string): string {
345
382
  const match = goMod.match(/^module\s+(\S+)/m);
346
383
  if (match) return match[1];
347
384
  }
348
- return 'github.com/workos/workos-go/v4';
385
+ return 'github.com/workos/workos-go/v2';
349
386
  }
350
387
 
351
388
  function generateGoImports(
352
389
  modulePath: string,
353
- servicePackages: Set<string>,
390
+ _servicePackages: Set<string>,
354
391
  needsJson: boolean,
355
- needsServicePkg: boolean,
392
+ _needsServicePkg: boolean,
356
393
  ): string {
394
+ // New emitter: flat package -- everything lives in the root module, no sub-packages.
357
395
  const lines: string[] = [];
358
396
  lines.push('import (');
359
397
  lines.push('\t"context"');
@@ -363,20 +401,21 @@ function generateGoImports(
363
401
  lines.push('\t"fmt"');
364
402
  lines.push('\t"os"');
365
403
  lines.push('');
366
- lines.push(`\tworkos "${modulePath}/pkg"`);
367
- if (needsServicePkg) {
368
- for (const pkg of [...servicePackages].sort()) {
369
- lines.push(`\t"${modulePath}/pkg/${pkg}"`);
370
- }
371
- }
404
+ lines.push(`\t"${modulePath}"`);
372
405
  lines.push(')');
373
406
  return lines.join('\n');
374
407
  }
375
408
 
376
- 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.
377
413
  const lines: string[] = [];
378
- lines.push(`${servicePackage}.${optsType}{`);
414
+ lines.push(`&workos.${paramsType}{`);
379
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;
380
419
  const goField = goFieldName(key);
381
420
  lines.push(`\t\t${goField}: ${goLiteral(value)},`);
382
421
  }
@@ -414,9 +453,9 @@ function generateGoCallBlock(
414
453
  pathParams: Record<string, string>,
415
454
  spec: any,
416
455
  callIndex: number,
456
+ accessorMap: Map<string, string>,
417
457
  ): string {
418
458
  const lines: string[] = [];
419
- const servicePackage = goServicePackageName(resolution.service);
420
459
  const method = resolution.method;
421
460
 
422
461
  // Build arguments
@@ -427,52 +466,63 @@ function generateGoCallBlock(
427
466
  args.push(`"${pathParams[p.name] || ''}"`);
428
467
  }
429
468
 
430
- // 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;
431
475
  if (op.requestBody) {
432
476
  const payload = generatePayload(op, spec);
433
477
  if (payload && Object.keys(payload).length > 0) {
434
- const optsType = `${method}Opts`;
435
- args.push(generateGoPayloadStruct(payload, optsType, servicePackage));
436
- }
437
- }
438
-
439
- // Paginated operations: pass opts with Limit=1
440
- if (op.pagination && !op.requestBody) {
441
- const extraParams = op.queryParams.filter((p: any) => !['limit', 'before', 'after', 'order'].includes(p.name));
442
- if (extraParams.length > 0) {
443
- // Match the emitter convention: List → ListFilterOpts, others → ${method}Opts
444
- const optsType = method === 'List' ? 'ListFilterOpts' : `${method}Opts`;
445
- args.push(`${servicePackage}.${optsType}{Limit: 1}`);
478
+ args.push(generateGoPayloadStruct(payload, paramsTypeName));
446
479
  } else {
447
- args.push(`${servicePackage}.ListOpts{Limit: 1}`);
480
+ // Even with empty payload, the method signature requires the params arg
481
+ args.push(`&workos.${paramsTypeName}{}`);
448
482
  }
483
+ } else if (op.pagination || hasQueryParams) {
484
+ // Paginated or query-param operations need a params struct
485
+ args.push(`&workos.${paramsTypeName}{}`);
449
486
  }
450
487
 
451
- // Determine the service accessor on the client
452
- const serviceProp = goExportedName(resolution.service);
488
+ // Service accessor: resolve from the generated SDK's actual accessor names
489
+ const serviceProp = resolveAccessorName(resolution.service, accessorMap);
453
490
 
454
491
  lines.push(`\t// Call ${callIndex}: ${op.httpMethod.toUpperCase()} ${op.path}`);
455
492
  lines.push(`\tfmt.Fprintf(os.Stderr, "CALL_START:${callIndex}\\n")`);
456
493
 
457
- // Determine return type: paginated and GET-with-response return (result, error),
458
- // DELETE returns just error
494
+ // Determine return type: paginated returns Iterator, DELETE and void/redirect return just error
459
495
  const isDelete = op.httpMethod === 'delete';
460
- const hasResponse = !isDelete;
461
-
462
- if (hasResponse) {
463
- 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(', ')})`);
464
514
  lines.push(`\tif err${callIndex} != nil {`);
465
515
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
466
516
  lines.push('\t} else {');
467
- lines.push(`\t\tjsonResult${callIndex}, _ := json.Marshal(result${callIndex})`);
468
- 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")`);
469
518
  lines.push('\t}');
470
519
  } else {
471
- lines.push(`\terr${callIndex} := client.${serviceProp}.${method}(${args.join(', ')})`);
520
+ lines.push(`\tresult${callIndex}, err${callIndex} := client.${serviceProp}().${method}(${args.join(', ')})`);
472
521
  lines.push(`\tif err${callIndex} != nil {`);
473
522
  lines.push(`\t\tfmt.Fprintf(os.Stderr, "CALL_ERROR:${callIndex}:%s\\n", err${callIndex}.Error())`);
474
523
  lines.push('\t} else {');
475
- 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}))`);
476
526
  lines.push('\t}');
477
527
  }
478
528
 
@@ -523,6 +573,11 @@ async function main(): Promise<void> {
523
573
  // Load manifest
524
574
  const manifest = loadManifest(sdkPath);
525
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
+
526
581
  const baseUrl = process.env.WORKOS_BASE_URL || spec.baseUrl;
527
582
 
528
583
  // Start capture proxy
@@ -633,7 +688,7 @@ async function main(): Promise<void> {
633
688
  // Generate all call blocks for this wave
634
689
  const callBlocks: string[] = [];
635
690
  for (const call of plannedCalls) {
636
- 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));
637
692
  }
638
693
 
639
694
  const imports = generateGoImports(modulePath, servicePackages, needsJson, needsServicePkg);
@@ -644,7 +699,7 @@ async function main(): Promise<void> {
644
699
  imports,
645
700
  '',
646
701
  'func main() {',
647
- `\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}"))`,
648
703
  '\tctx := context.Background()',
649
704
  '',
650
705
  ...callBlocks,
@@ -660,8 +715,10 @@ async function main(): Promise<void> {
660
715
 
661
716
  // Step 1: Build (sync — no proxy needed during compilation)
662
717
  let buildError: string | null = null;
718
+
719
+ // Run go mod tidy first to resolve dependencies
663
720
  try {
664
- execSync('go build -o smoke-driver main.go', {
721
+ execSync('go mod tidy', {
665
722
  cwd: tmpDir,
666
723
  timeout: 120_000,
667
724
  env: {
@@ -673,9 +730,26 @@ async function main(): Promise<void> {
673
730
  });
674
731
  } catch (err: any) {
675
732
  const stderr = typeof err.stderr === 'string' ? err.stderr : '';
676
- 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(' ')}`;
677
734
  }
678
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
+
679
753
  if (buildError) {
680
754
  // Build failure affects entire wave
681
755
  const elapsed = Date.now() - waveStart;