@workos/oagen-emitters 0.3.0 → 0.5.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 (128) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint.yml +1 -1
  3. package/.github/workflows/release-please.yml +2 -2
  4. package/.github/workflows/release.yml +1 -1
  5. package/.husky/pre-push +11 -0
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +15 -0
  9. package/README.md +35 -224
  10. package/dist/index.d.mts +12 -1
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -12737
  13. package/dist/plugin-BSop9f9z.mjs +21471 -0
  14. package/dist/plugin-BSop9f9z.mjs.map +1 -0
  15. package/dist/plugin.d.mts +7 -0
  16. package/dist/plugin.d.mts.map +1 -0
  17. package/dist/plugin.mjs +2 -0
  18. package/docs/sdk-architecture/dotnet.md +336 -0
  19. package/oagen.config.ts +5 -343
  20. package/package.json +10 -34
  21. package/smoke/sdk-dotnet.ts +45 -12
  22. package/src/dotnet/client.ts +89 -0
  23. package/src/dotnet/enums.ts +323 -0
  24. package/src/dotnet/fixtures.ts +236 -0
  25. package/src/dotnet/index.ts +248 -0
  26. package/src/dotnet/manifest.ts +36 -0
  27. package/src/dotnet/models.ts +320 -0
  28. package/src/dotnet/naming.ts +368 -0
  29. package/src/dotnet/resources.ts +943 -0
  30. package/src/dotnet/tests.ts +713 -0
  31. package/src/dotnet/type-map.ts +228 -0
  32. package/src/dotnet/wrappers.ts +197 -0
  33. package/src/go/client.ts +35 -3
  34. package/src/go/enums.ts +4 -0
  35. package/src/go/index.ts +15 -7
  36. package/src/go/models.ts +6 -1
  37. package/src/go/naming.ts +5 -17
  38. package/src/go/resources.ts +534 -73
  39. package/src/go/tests.ts +39 -3
  40. package/src/go/type-map.ts +8 -3
  41. package/src/go/wrappers.ts +79 -21
  42. package/src/index.ts +15 -0
  43. package/src/kotlin/client.ts +58 -0
  44. package/src/kotlin/enums.ts +189 -0
  45. package/src/kotlin/index.ts +92 -0
  46. package/src/kotlin/manifest.ts +55 -0
  47. package/src/kotlin/models.ts +486 -0
  48. package/src/kotlin/naming.ts +229 -0
  49. package/src/kotlin/overrides.ts +25 -0
  50. package/src/kotlin/resources.ts +998 -0
  51. package/src/kotlin/tests.ts +1133 -0
  52. package/src/kotlin/type-map.ts +123 -0
  53. package/src/kotlin/wrappers.ts +168 -0
  54. package/src/node/client.ts +84 -7
  55. package/src/node/field-plan.ts +12 -14
  56. package/src/node/fixtures.ts +39 -3
  57. package/src/node/index.ts +1 -0
  58. package/src/node/models.ts +281 -37
  59. package/src/node/resources.ts +319 -95
  60. package/src/node/tests.ts +108 -29
  61. package/src/node/type-map.ts +1 -31
  62. package/src/node/utils.ts +96 -6
  63. package/src/node/wrappers.ts +31 -1
  64. package/src/php/client.ts +11 -3
  65. package/src/php/models.ts +0 -33
  66. package/src/php/naming.ts +2 -21
  67. package/src/php/resources.ts +275 -19
  68. package/src/php/tests.ts +118 -18
  69. package/src/php/type-map.ts +16 -2
  70. package/src/php/wrappers.ts +7 -2
  71. package/src/plugin.ts +50 -0
  72. package/src/python/client.ts +50 -32
  73. package/src/python/enums.ts +35 -10
  74. package/src/python/index.ts +35 -27
  75. package/src/python/models.ts +139 -2
  76. package/src/python/naming.ts +2 -22
  77. package/src/python/resources.ts +234 -17
  78. package/src/python/tests.ts +260 -16
  79. package/src/python/type-map.ts +16 -2
  80. package/src/ruby/client.ts +238 -0
  81. package/src/ruby/enums.ts +149 -0
  82. package/src/ruby/index.ts +93 -0
  83. package/src/ruby/manifest.ts +35 -0
  84. package/src/ruby/models.ts +360 -0
  85. package/src/ruby/naming.ts +187 -0
  86. package/src/ruby/rbi.ts +313 -0
  87. package/src/ruby/resources.ts +799 -0
  88. package/src/ruby/tests.ts +459 -0
  89. package/src/ruby/type-map.ts +97 -0
  90. package/src/ruby/wrappers.ts +161 -0
  91. package/src/shared/model-utils.ts +357 -16
  92. package/src/shared/naming-utils.ts +83 -0
  93. package/src/shared/non-spec-services.ts +13 -0
  94. package/src/shared/resolved-ops.ts +75 -1
  95. package/src/shared/wrapper-utils.ts +12 -1
  96. package/test/dotnet/client.test.ts +121 -0
  97. package/test/dotnet/enums.test.ts +193 -0
  98. package/test/dotnet/errors.test.ts +9 -0
  99. package/test/dotnet/manifest.test.ts +82 -0
  100. package/test/dotnet/models.test.ts +258 -0
  101. package/test/dotnet/resources.test.ts +387 -0
  102. package/test/dotnet/tests.test.ts +202 -0
  103. package/test/entrypoint.test.ts +89 -0
  104. package/test/go/client.test.ts +6 -6
  105. package/test/go/resources.test.ts +156 -7
  106. package/test/kotlin/models.test.ts +135 -0
  107. package/test/kotlin/resources.test.ts +210 -0
  108. package/test/kotlin/tests.test.ts +176 -0
  109. package/test/node/client.test.ts +74 -0
  110. package/test/node/models.test.ts +134 -1
  111. package/test/node/resources.test.ts +343 -34
  112. package/test/node/utils.test.ts +140 -0
  113. package/test/php/client.test.ts +2 -1
  114. package/test/php/models.test.ts +5 -4
  115. package/test/php/resources.test.ts +103 -0
  116. package/test/php/tests.test.ts +67 -0
  117. package/test/plugin.test.ts +50 -0
  118. package/test/python/client.test.ts +56 -0
  119. package/test/python/models.test.ts +99 -0
  120. package/test/python/resources.test.ts +294 -0
  121. package/test/python/tests.test.ts +91 -0
  122. package/test/ruby/client.test.ts +81 -0
  123. package/test/ruby/resources.test.ts +386 -0
  124. package/test/shared/resolved-ops.test.ts +122 -0
  125. package/tsdown.config.ts +1 -1
  126. package/dist/index.mjs.map +0 -1
  127. package/scripts/generate-php.js +0 -13
  128. package/scripts/git-push-with-published-oagen.sh +0 -21
package/oagen.config.ts CHANGED
@@ -1,347 +1,9 @@
1
- import type { OagenConfig, OperationHint } from '@workos/oagen';
2
- import { toCamelCase } from '@workos/oagen';
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';
7
- import { nodeExtractor } from './src/compat/extractors/node.js';
8
- import { rubyExtractor } from './src/compat/extractors/ruby.js';
9
- import { pythonExtractor } from './src/compat/extractors/python.js';
10
- import { phpExtractor } from './src/compat/extractors/php.js';
11
- import { goExtractor } from './src/compat/extractors/go.js';
12
- import { rustExtractor } from './src/compat/extractors/rust.js';
13
- import { kotlinExtractor } from './src/compat/extractors/kotlin.js';
14
- import { dotnetExtractor } from './src/compat/extractors/dotnet.js';
15
- import { elixirExtractor } from './src/compat/extractors/elixir.js';
16
-
17
- /**
18
- * NestJS-style operationId transform. Strips "Controller" and extracts the
19
- * action after the first underscore: `FooController_bar` → `bar`.
20
- */
21
- function nestjsOperationIdTransform(id: string): string {
22
- const stripped = id.replace(/Controller/g, '');
23
- const idx = stripped.indexOf('_');
24
- return idx !== -1 ? toCamelCase(stripped.slice(idx + 1)) : toCamelCase(stripped);
25
- }
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
- };
1
+ import type { OagenConfig } from '@workos/oagen';
2
+ import { workosEmittersPlugin } from './src/plugin.js';
300
3
 
4
+ // Minimal config for local emitter development.
5
+ // The canonical consumer config lives in openapi-spec/oagen.config.ts.
301
6
  const config: OagenConfig = {
302
- emitters: [nodeEmitter, pythonEmitter, phpEmitter, goEmitter],
303
- extractors: [
304
- nodeExtractor,
305
- rubyExtractor,
306
- pythonExtractor,
307
- phpExtractor,
308
- goExtractor,
309
- rustExtractor,
310
- kotlinExtractor,
311
- dotnetExtractor,
312
- elixirExtractor,
313
- ],
314
- smokeRunners: {
315
- node: './smoke/sdk-node.ts',
316
- ruby: './smoke/sdk-ruby.ts',
317
- python: './smoke/sdk-python.ts',
318
- php: './smoke/sdk-php.ts',
319
- go: './smoke/sdk-go.ts',
320
- rust: './smoke/sdk-rust.ts',
321
- elixir: './smoke/sdk-elixir.ts',
322
- kotlin: './smoke/sdk-kotlin.ts',
323
- dotnet: './smoke/sdk-dotnet.ts',
324
- },
325
- docUrl: 'https://workos.com/docs',
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,
7
+ ...workosEmittersPlugin,
346
8
  };
347
9
  export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -15,6 +15,10 @@
15
15
  ".": {
16
16
  "types": "./dist/index.d.mts",
17
17
  "import": "./dist/index.mjs"
18
+ },
19
+ "./plugin": {
20
+ "types": "./dist/plugin.d.mts",
21
+ "import": "./dist/plugin.mjs"
18
22
  }
19
23
  },
20
24
  "scripts": {
@@ -28,45 +32,17 @@
28
32
  "test": "vitest run",
29
33
  "test:watch": "vitest",
30
34
  "typecheck": "tsc --noEmit",
31
- "git:push": "sh ./scripts/git-push-with-published-oagen.sh",
32
35
  "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'",
35
- "prepare": "husky",
36
- "sdk:generate:dotnet": "oagen generate --lang dotnet --output ./sdk-dotnet --namespace workos --api-surface ./sdk-dotnet-surface.json",
37
- "sdk:verify:dotnet": "oagen verify --lang dotnet --output ./sdk-dotnet --api-surface ./sdk-dotnet-surface.json",
38
- "sdk:extract:dotnet": "oagen extract --lang dotnet --sdk-path ../backend/workos-dotnet --output ./sdk-dotnet-surface.json",
39
- "sdk:generate:elixir": "oagen generate --lang elixir --output ./sdk-elixir --namespace workos --api-surface ./sdk-elixir-surface.json",
40
- "sdk:verify:elixir": "oagen verify --lang elixir --output ./sdk-elixir --api-surface ./sdk-elixir-surface.json",
41
- "sdk:extract:elixir": "oagen extract --lang elixir --sdk-path ../backend/workos-elixir --output ./sdk-elixir-surface.json",
42
- "sdk:generate:go": "oagen generate --lang go --output ./sdk-go --namespace workos --api-surface ./sdk-go-surface.json",
43
- "sdk:verify:go": "oagen verify --lang go --output ./sdk-go --api-surface ./sdk-go-surface.json",
44
- "sdk:extract:go": "oagen extract --lang go --sdk-path ../backend/workos-go --output ./sdk-go-surface.json",
45
- "sdk:generate:kotlin": "oagen generate --lang kotlin --output ./sdk-kotlin --namespace workos --api-surface ./sdk-kotlin-surface.json",
46
- "sdk:verify:kotlin": "oagen verify --lang kotlin --output ./sdk-kotlin --api-surface ./sdk-kotlin-surface.json",
47
- "sdk:extract:kotlin": "oagen extract --lang kotlin --sdk-path ../backend/workos-kotlin --output ./sdk-kotlin-surface.json",
48
- "sdk:generate:node": "oagen generate --lang node --output ./sdk-node --namespace workos --api-surface ./sdk-node-surface.json",
49
- "sdk:verify:node": "oagen verify --lang node --output ./sdk-node --api-surface ./sdk-node-surface.json",
50
- "sdk:extract:node": "oagen extract --lang node --sdk-path ../backend/workos-node --output ./sdk-node-surface.json",
51
- "sdk:generate:php": "node scripts/generate-php.js",
52
- "sdk:verify:php": "oagen verify --lang php --output ./sdk-php --api-surface ./sdk-php-surface.json",
53
- "sdk:extract:php": "oagen extract --lang php --sdk-path ../backend/workos-php --output ./sdk-php-surface.json",
54
- "sdk:generate:python": "oagen generate --lang python --output ./sdk-python --namespace workos --api-surface ./sdk-python-surface.json",
55
- "sdk:verify:python": "oagen verify --lang python --output ./sdk-python --api-surface ./sdk-python-surface.json",
56
- "sdk:extract:python": "oagen extract --lang python --sdk-path ../backend/workos-python --output ./sdk-python-surface.json",
57
- "sdk:generate:ruby": "oagen generate --lang ruby --output ./sdk-ruby --namespace workos --api-surface ./sdk-ruby-surface.json",
58
- "sdk:verify:ruby": "oagen verify --lang ruby --output ./sdk-ruby --api-surface ./sdk-ruby-surface.json",
59
- "sdk:extract:ruby": "oagen extract --lang ruby --sdk-path ../backend/workos-ruby --output ./sdk-ruby-surface.json",
60
- "sdk:generate:rust": "oagen generate --lang rust --output ./sdk-rust --namespace workos --api-surface ./sdk-rust-surface.json",
61
- "sdk:verify:rust": "oagen verify --lang rust --output ./sdk-rust --api-surface ./sdk-rust-surface.json",
62
- "sdk:extract:rust": "oagen extract --lang rust --sdk-path ../backend/workos-rust --output ./sdk-rust-surface.json"
36
+ "dev:link": "npm run oagen:build:local && npm link --prefix ../oagen && npm link @workos/oagen",
37
+ "dev:unlink": "npm install",
38
+ "prepare": "husky"
63
39
  },
64
40
  "devDependencies": {
65
41
  "@commitlint/cli": "^20.5.0",
66
42
  "@commitlint/config-conventional": "^20.5.0",
67
43
  "@types/node": "^25.3.3",
68
44
  "husky": "^9.1.7",
69
- "oxfmt": "^0.36.0",
45
+ "oxfmt": "^0.45.0",
70
46
  "oxlint": "^1.51.0",
71
47
  "prettier": "^3.8.1",
72
48
  "tsdown": "^0.21.5",
@@ -78,6 +54,6 @@
78
54
  "node": ">=24.10.0"
79
55
  },
80
56
  "dependencies": {
81
- "@workos/oagen": "^0.5.0"
57
+ "@workos/oagen": "^0.7.0"
82
58
  }
83
59
  }
@@ -405,14 +405,33 @@ function buildBatchedCSharpScript(port: number, ns: string, calls: PlannedCall[]
405
405
  // ---------------------------------------------------------------------------
406
406
 
407
407
  /**
408
- * Find the .csproj file in the SDK directory. Returns the full resolved path.
408
+ * Find the .csproj file in the SDK directory. Searches the root first, then
409
+ * common subdirectory patterns (src/{Name}/) used by the generated SDK layout.
409
410
  */
410
411
  function findCsproj(sdkPath: string): string {
411
- const files = readdirSync(sdkPath).filter((f) => f.endsWith('.csproj'));
412
- if (files.length === 0) {
413
- throw new Error(`No .csproj file found in ${sdkPath}`);
412
+ // Check root directory first
413
+ const rootFiles = readdirSync(sdkPath).filter((f) => f.endsWith('.csproj'));
414
+ if (rootFiles.length > 0) {
415
+ return resolve(sdkPath, rootFiles[0]);
414
416
  }
415
- return resolve(sdkPath, files[0]);
417
+
418
+ // Check src/ subdirectories (generated SDK layout: src/WorkOS.net/WorkOS.net.csproj)
419
+ const srcDir = resolve(sdkPath, 'src');
420
+ if (existsSync(srcDir)) {
421
+ for (const subdir of readdirSync(srcDir)) {
422
+ const subdirPath = resolve(srcDir, subdir);
423
+ try {
424
+ const subFiles = readdirSync(subdirPath).filter((f) => f.endsWith('.csproj'));
425
+ if (subFiles.length > 0) {
426
+ return resolve(subdirPath, subFiles[0]);
427
+ }
428
+ } catch {
429
+ // Not a directory, skip
430
+ }
431
+ }
432
+ }
433
+
434
+ throw new Error(`No .csproj file found in ${sdkPath} or ${sdkPath}/src/*/`);
416
435
  }
417
436
 
418
437
  /**
@@ -429,6 +448,18 @@ function detectNamespace(sdkPath: string): string {
429
448
  return base.replace('.csproj', '');
430
449
  }
431
450
 
451
+ /**
452
+ * Detect the assembly name from the .csproj file's AssemblyName property.
453
+ * Falls back to namespace if not found.
454
+ */
455
+ function detectAssemblyName(sdkPath: string): string {
456
+ const csprojPath = findCsproj(sdkPath);
457
+ const content = readFileSync(csprojPath, 'utf-8');
458
+ const match = content.match(/<AssemblyName>([^<]+)<\/AssemblyName>/);
459
+ if (match) return match[1];
460
+ return detectNamespace(sdkPath);
461
+ }
462
+
432
463
  // ---------------------------------------------------------------------------
433
464
  // .NET project generation
434
465
  // ---------------------------------------------------------------------------
@@ -590,9 +621,10 @@ async function main(): Promise<void> {
590
621
  const spec = await parseSpec(specPath);
591
622
  console.log(`Spec: ${spec.name} v${spec.version}`);
592
623
 
593
- // Detect SDK namespace
624
+ // Detect SDK namespace and assembly name
594
625
  const ns = detectNamespace(sdkPath);
595
- console.log(`SDK namespace: ${ns}`);
626
+ const assemblyName = detectAssemblyName(sdkPath);
627
+ console.log(`SDK namespace: ${ns}, assembly: ${assemblyName}`);
596
628
 
597
629
  // Load manifest
598
630
  const manifest = loadManifest(sdkPath);
@@ -620,10 +652,12 @@ async function main(): Promise<void> {
620
652
 
621
653
  // Step 1: Build the SDK project to a DLL
622
654
  const sdkCsprojPath = findCsproj(sdkPath);
655
+ const sdkCsprojDir = sdkCsprojPath.substring(0, sdkCsprojPath.lastIndexOf('/'));
656
+ const sdkDllDir = resolve(sdkPath, 'bin/Release');
623
657
  console.log('Building SDK...');
624
658
  try {
625
- execSync(`dotnet build "${sdkCsprojPath}" -c Release -o "${resolve(sdkPath, 'bin/Release/net8.0')}"`, {
626
- cwd: sdkPath,
659
+ execSync(`dotnet build "${sdkCsprojPath}" -c Release -o "${sdkDllDir}"`, {
660
+ cwd: sdkCsprojDir,
627
661
  timeout: 120000,
628
662
  stdio: ['pipe', 'pipe', 'pipe'],
629
663
  env: { ...process.env, DOTNET_NOLOGO: '1' },
@@ -635,9 +669,8 @@ async function main(): Promise<void> {
635
669
  process.exit(1);
636
670
  }
637
671
 
638
- // Find the SDK DLL
639
- const sdkDllDir = resolve(sdkPath, 'bin/Release/net8.0');
640
- const sdkDll = resolve(sdkDllDir, `${ns}.dll`);
672
+ // Find the SDK DLL (use AssemblyName, not namespace)
673
+ const sdkDll = resolve(sdkDllDir, `${assemblyName}.dll`);
641
674
 
642
675
  // Step 2: Bootstrap the driver project referencing the built DLL
643
676
  mkdirSync(tmpDir, { recursive: true });
@@ -0,0 +1,89 @@
1
+ import type { ApiSpec, EmitterContext, GeneratedFile, Service } from '@workos/oagen';
2
+ import { toPascalCase, toSnakeCase } from '@workos/oagen';
3
+ import { resolveResourceClassName } from './resources.js';
4
+ import { className, serviceTypeName, humanize } from './naming.js';
5
+ import { getMountTarget } from '../shared/resolved-ops.js';
6
+
7
+ /**
8
+ * Generate the C# client file with service accessors.
9
+ * Produces: WorkOSClient.Generated.cs (partial class with service properties).
10
+ */
11
+ export function generateClient(spec: ApiSpec, ctx: EmitterContext): GeneratedFile[] {
12
+ return [generateClientFile(spec, ctx)];
13
+ }
14
+
15
+ /**
16
+ * Deduplicate services by mount target.
17
+ */
18
+ function deduplicateByMount(services: Service[], ctx: EmitterContext): Service[] {
19
+ const byTarget = new Map<string, Service>();
20
+ for (const s of services) {
21
+ const target = getMountTarget(s, ctx);
22
+ const existing = byTarget.get(target);
23
+ if (!existing || toPascalCase(s.name) === target) {
24
+ byTarget.set(target, s);
25
+ }
26
+ }
27
+ return [...byTarget.values()];
28
+ }
29
+
30
+ /**
31
+ * Build map of service name -> accessor property name.
32
+ */
33
+ export function buildServiceAccessPaths(services: Service[], ctx: EmitterContext): Map<string, string> {
34
+ const topLevel = deduplicateByMount(services, ctx);
35
+ const paths = new Map<string, string>();
36
+
37
+ for (const service of topLevel) {
38
+ const resolvedName = resolveResourceClassName(service, ctx);
39
+ const prop = toSnakeCase(resolvedName);
40
+ paths.set(service.name, prop);
41
+ }
42
+
43
+ // Also map mount targets
44
+ for (const service of services) {
45
+ const target = getMountTarget(service, ctx);
46
+ if (!paths.has(target)) {
47
+ const existing = paths.get(service.name);
48
+ if (existing) paths.set(target, existing);
49
+ }
50
+ }
51
+
52
+ return paths;
53
+ }
54
+
55
+ function generateClientFile(spec: ApiSpec, ctx: EmitterContext): GeneratedFile {
56
+ const topLevel = deduplicateByMount(spec.services, ctx);
57
+ const lines: string[] = [];
58
+
59
+ lines.push(`namespace ${ctx.namespacePascal}`);
60
+ lines.push('{');
61
+ lines.push(' /// <summary>');
62
+ lines.push(' /// Generated service accessors for WorkOSClient.');
63
+ lines.push(' /// </summary>');
64
+ lines.push(' public partial class WorkOSClient');
65
+ lines.push(' {');
66
+
67
+ // Service properties with lazy initialization
68
+ for (const service of topLevel) {
69
+ const resolvedName = resolveResourceClassName(service, ctx);
70
+ const propName = className(resolvedName);
71
+ const svcType = serviceTypeName(resolvedName);
72
+ const backingField = propName.charAt(0).toLowerCase() + propName.slice(1);
73
+ const human = humanize(resolvedName);
74
+ lines.push(` private ${svcType} ${backingField};`);
75
+ lines.push('');
76
+ lines.push(` /// <summary>Gets the <see cref="${svcType}"/> for ${human} API operations.</summary>`);
77
+ lines.push(` public virtual ${svcType} ${propName} => this.${backingField} ??= new ${svcType}(this);`);
78
+ lines.push('');
79
+ }
80
+
81
+ lines.push(' }');
82
+ lines.push('}');
83
+
84
+ return {
85
+ path: 'Client/WorkOSClient.Generated.cs',
86
+ content: lines.join('\n'),
87
+ overwriteExisting: true,
88
+ };
89
+ }