@workos/oagen-emitters 0.3.0 → 0.4.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 (65) hide show
  1. package/.release-please-manifest.json +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/index.d.mts +4 -1
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +3288 -791
  6. package/dist/index.mjs.map +1 -1
  7. package/docs/sdk-architecture/dotnet.md +336 -0
  8. package/oagen.config.ts +42 -12
  9. package/package.json +2 -2
  10. package/smoke/sdk-dotnet.ts +45 -12
  11. package/src/dotnet/client.ts +89 -0
  12. package/src/dotnet/enums.ts +323 -0
  13. package/src/dotnet/fixtures.ts +236 -0
  14. package/src/dotnet/index.ts +246 -0
  15. package/src/dotnet/manifest.ts +36 -0
  16. package/src/dotnet/models.ts +344 -0
  17. package/src/dotnet/naming.ts +330 -0
  18. package/src/dotnet/resources.ts +622 -0
  19. package/src/dotnet/tests.ts +693 -0
  20. package/src/dotnet/type-map.ts +201 -0
  21. package/src/dotnet/wrappers.ts +186 -0
  22. package/src/go/index.ts +5 -2
  23. package/src/go/naming.ts +5 -17
  24. package/src/index.ts +1 -0
  25. package/src/kotlin/client.ts +53 -0
  26. package/src/kotlin/enums.ts +162 -0
  27. package/src/kotlin/index.ts +92 -0
  28. package/src/kotlin/manifest.ts +55 -0
  29. package/src/kotlin/models.ts +395 -0
  30. package/src/kotlin/naming.ts +223 -0
  31. package/src/kotlin/overrides.ts +25 -0
  32. package/src/kotlin/resources.ts +667 -0
  33. package/src/kotlin/tests.ts +1019 -0
  34. package/src/kotlin/type-map.ts +123 -0
  35. package/src/kotlin/wrappers.ts +168 -0
  36. package/src/node/client.ts +50 -0
  37. package/src/node/index.ts +1 -0
  38. package/src/node/resources.ts +164 -44
  39. package/src/node/tests.ts +37 -7
  40. package/src/php/client.ts +11 -3
  41. package/src/php/naming.ts +2 -21
  42. package/src/php/resources.ts +81 -6
  43. package/src/php/tests.ts +93 -17
  44. package/src/php/wrappers.ts +1 -0
  45. package/src/python/client.ts +37 -29
  46. package/src/python/enums.ts +7 -7
  47. package/src/python/models.ts +1 -1
  48. package/src/python/naming.ts +2 -22
  49. package/src/shared/model-utils.ts +232 -15
  50. package/src/shared/naming-utils.ts +47 -0
  51. package/src/shared/wrapper-utils.ts +12 -1
  52. package/test/dotnet/client.test.ts +121 -0
  53. package/test/dotnet/enums.test.ts +193 -0
  54. package/test/dotnet/errors.test.ts +9 -0
  55. package/test/dotnet/manifest.test.ts +82 -0
  56. package/test/dotnet/models.test.ts +260 -0
  57. package/test/dotnet/resources.test.ts +255 -0
  58. package/test/dotnet/tests.test.ts +202 -0
  59. package/test/kotlin/models.test.ts +135 -0
  60. package/test/kotlin/tests.test.ts +176 -0
  61. package/test/node/client.test.ts +74 -0
  62. package/test/node/resources.test.ts +216 -15
  63. package/test/php/client.test.ts +2 -1
  64. package/test/php/resources.test.ts +38 -0
  65. package/test/php/tests.test.ts +67 -0
@@ -0,0 +1,336 @@
1
+ # C# / .NET SDK Architecture
2
+
3
+ Target: .NET 8.0 | Serialization: Newtonsoft.Json | Test: xUnit + Moq
4
+
5
+ ## Architecture Overview
6
+
7
+ The SDK follows a service-per-domain pattern. A static `WorkOS` entry point holds a singleton `WorkOSClient`. Each service (e.g., `OrganizationsService`) inherits from a shared `Service` base class that lazily resolves the client. All IO is async with `CancellationToken` support.
8
+
9
+ **Runtime files (hand-maintained, `@oagen-ignore-file`):**
10
+
11
+ - `WorkOS.cs` — static entry point
12
+ - `Client/WorkOSClient.cs` — HTTP execution, retry, error translation
13
+ - `Client/_interfaces/WorkOSOptions.cs` — client config
14
+ - `Client/_interfaces/WorkOSRequest.cs` — request DTO
15
+ - `Client/Utilities/RequestUtilities.cs` — JSON/query serialization
16
+ - `Services/Webhooks/WebhookService.cs` — webhook signature verification helper
17
+ - `Services/Webhooks/Entities/Webhook.cs` — webhook event envelope used by the helper
18
+ - `Services/Webhooks/Exceptions/WorkOSWebhookException.cs` — webhook verification exception
19
+ - `Services/_common/Service.cs` — base service class
20
+ - `Services/_common/_interfaces/BaseOptions.cs` — marker base for options
21
+ - `Services/_common/_interfaces/ListOptions.cs` — pagination base options
22
+ - `Services/_common/Entities/WorkOSList.cs` — pagination wrapper
23
+ - `Services/_common/Entities/ListMetadata.cs` — cursor metadata
24
+ - `Services/_common/Enums/PaginationOrder.cs` — asc/desc enum
25
+
26
+ **Generated files (emitter output):**
27
+
28
+ - `Services/{Mount}/Entities/*.cs` — model classes
29
+ - `Services/{Mount}/Enums/*.cs` — enum types
30
+ - `Services/{Mount}/{Mount}Service.cs` — service class with methods
31
+ - `Services/{Mount}/_interfaces/*Options.cs` — request option classes
32
+ - `test/WorkOSTests/Tests/*Test.cs` — generated service tests
33
+ - `test/WorkOSTests/testdata/*.json` — generated fixtures
34
+ - `test/WorkOSTests/xunit.runner.json` — generated xUnit runner config
35
+
36
+ ## Naming Conventions
37
+
38
+ | IR Concept | C# Convention | Example |
39
+ | -------------- | ---------------------------- | --------------------------- |
40
+ | Model name | PascalCase | `Organization` |
41
+ | Enum name | PascalCase | `ConnectionState` |
42
+ | Field/property | PascalCase | `EmailVerified` |
43
+ | Method | PascalCase (no Async suffix) | `GetOrganization` |
44
+ | File | PascalCase.cs | `Organization.cs` |
45
+ | Service class | `{Mount}Service` | `OrganizationsService` |
46
+ | Options class | `{Action}{Entity}Options` | `CreateOrganizationOptions` |
47
+ | Namespace | `WorkOS` | — |
48
+
49
+ ## Type Mapping
50
+
51
+ | IR TypeRef | C# Type |
52
+ | ------------------------------ | ----------------------- |
53
+ | `primitive:string` | `string` |
54
+ | `primitive:string` (date-time) | `string` |
55
+ | `primitive:string` (uuid) | `string` |
56
+ | `primitive:string` (binary) | `byte[]` |
57
+ | `primitive:integer` | `int` |
58
+ | `primitive:integer` (int64) | `long` |
59
+ | `primitive:number` | `double` |
60
+ | `primitive:boolean` | `bool` |
61
+ | `primitive:unknown` | `object` |
62
+ | `model:Foo` | `Foo` (reference type) |
63
+ | `enum:Foo` | `Foo` (value type) |
64
+ | `array` | `List<T>` |
65
+ | `map` | `Dictionary<string, T>` |
66
+ | `nullable` (value type) | `T?` |
67
+ | `nullable` (reference type) | `T` |
68
+ | `union` (single) | that type |
69
+ | `union` (multiple) | `object` |
70
+
71
+ ## Model Pattern
72
+
73
+ ```csharp
74
+ namespace WorkOS
75
+ {
76
+ using Newtonsoft.Json;
77
+
78
+ /// <summary>Represents an organization.</summary>
79
+ public class Organization
80
+ {
81
+ [JsonProperty("id")]
82
+ public string Id { get; set; }
83
+
84
+ [JsonProperty("name")]
85
+ public string Name { get; set; }
86
+
87
+ [JsonProperty("allow_profiles_outside_organization")]
88
+ public bool AllowProfilesOutsideOrganization { get; set; }
89
+
90
+ [JsonProperty("created_at")]
91
+ public string CreatedAt { get; set; }
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Enum Pattern
97
+
98
+ ```csharp
99
+ namespace WorkOS
100
+ {
101
+ using System.Runtime.Serialization;
102
+ using Newtonsoft.Json;
103
+ using Newtonsoft.Json.Converters;
104
+
105
+ [JsonConverter(typeof(StringEnumConverter))]
106
+ public enum OrganizationDomainState
107
+ {
108
+ [EnumMember(Value = "failed")]
109
+ Failed,
110
+
111
+ [EnumMember(Value = "pending")]
112
+ Pending,
113
+
114
+ [EnumMember(Value = "verified")]
115
+ Verified,
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## Resource/Service Pattern
121
+
122
+ ```csharp
123
+ namespace WorkOS
124
+ {
125
+ using System.Net.Http;
126
+ using System.Threading;
127
+ using System.Threading.Tasks;
128
+
129
+ public class OrganizationsService : Service
130
+ {
131
+ public OrganizationsService() { }
132
+ public OrganizationsService(WorkOSClient client) : base(client) { }
133
+
134
+ public async Task<Organization> GetOrganization(
135
+ string id,
136
+ CancellationToken cancellationToken = default)
137
+ {
138
+ var request = new WorkOSRequest
139
+ {
140
+ Method = HttpMethod.Get,
141
+ Path = $"/organizations/{id}",
142
+ };
143
+ return await this.Client.MakeAPIRequest<Organization>(request, cancellationToken);
144
+ }
145
+
146
+ public async Task<WorkOSList<Organization>> ListOrganizations(
147
+ ListOrganizationsOptions options = null,
148
+ CancellationToken cancellationToken = default)
149
+ {
150
+ var request = new WorkOSRequest
151
+ {
152
+ Method = HttpMethod.Get,
153
+ Path = "/organizations",
154
+ Options = options,
155
+ };
156
+ return await this.Client.MakeAPIRequest<WorkOSList<Organization>>(request, cancellationToken);
157
+ }
158
+
159
+ public async Task<Organization> CreateOrganization(
160
+ CreateOrganizationOptions options,
161
+ CancellationToken cancellationToken = default)
162
+ {
163
+ var request = new WorkOSRequest
164
+ {
165
+ Method = HttpMethod.Post,
166
+ Path = "/organizations",
167
+ Options = options,
168
+ };
169
+ return await this.Client.MakeAPIRequest<Organization>(request, cancellationToken);
170
+ }
171
+
172
+ public async Task DeleteOrganization(
173
+ string id,
174
+ CancellationToken cancellationToken = default)
175
+ {
176
+ var request = new WorkOSRequest
177
+ {
178
+ Method = HttpMethod.Delete,
179
+ Path = $"/organizations/{id}",
180
+ };
181
+ await this.Client.MakeRawAPIRequest(request, cancellationToken);
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ ## Options Pattern
188
+
189
+ ```csharp
190
+ namespace WorkOS
191
+ {
192
+ using Newtonsoft.Json;
193
+
194
+ public class CreateOrganizationOptions : BaseOptions
195
+ {
196
+ [JsonProperty("name")]
197
+ public string Name { get; set; }
198
+
199
+ [JsonProperty("domain_data")]
200
+ public List<OrganizationDomainDataOptions> DomainData { get; set; }
201
+ }
202
+
203
+ public class ListOrganizationsOptions : ListOptions
204
+ {
205
+ [JsonProperty("domains")]
206
+ public string[] Domains { get; set; }
207
+ }
208
+ }
209
+ ```
210
+
211
+ ## Pagination Pattern
212
+
213
+ ```csharp
214
+ // Returned by list methods — NOT an iterator
215
+ public class WorkOSList<T>
216
+ {
217
+ [JsonProperty("data")]
218
+ public List<T> Data { get; set; }
219
+
220
+ [JsonProperty("list_metadata")]
221
+ public ListMetadata ListMetadata { get; set; }
222
+ }
223
+ ```
224
+
225
+ ## Error Handling
226
+
227
+ The runtime translates HTTP status codes to SDK-native exceptions:
228
+
229
+ - `AuthenticationError` (401)
230
+ - `NotFoundError` (404)
231
+ - `UnprocessableEntityError` (422)
232
+ - `RateLimitExceededError` (429)
233
+ - `ServerError` (500+)
234
+
235
+ These are hand-maintained in the runtime. The emitter generates error-handling _tests_, not the error classes themselves.
236
+
237
+ ## Hidden Parameter Injection
238
+
239
+ For operations with defaults/inferFromClient, the service method sets properties on the options before making the request:
240
+
241
+ ```csharp
242
+ public async Task<AuthenticationResponse> AuthenticateWithPassword(
243
+ AuthenticateWithPasswordOptions options,
244
+ CancellationToken cancellationToken = default)
245
+ {
246
+ options.GrantType = "password";
247
+ options.ClientId = this.Client.ClientId;
248
+ options.ClientSecret = this.Client.ApiKey;
249
+ var request = new WorkOSRequest
250
+ {
251
+ Method = HttpMethod.Post,
252
+ Path = "/user_management/authenticate",
253
+ Options = options,
254
+ };
255
+ return await this.Client.MakeAPIRequest<AuthenticationResponse>(request, cancellationToken);
256
+ }
257
+ ```
258
+
259
+ ## Testing Pattern
260
+
261
+ xUnit + Moq with HttpMock utility:
262
+
263
+ ```csharp
264
+ public class OrganizationsServiceTest
265
+ {
266
+ private readonly HttpMock httpMock;
267
+ private readonly OrganizationsService service;
268
+
269
+ public OrganizationsServiceTest()
270
+ {
271
+ this.httpMock = new HttpMock();
272
+ var client = new WorkOSClient(new WorkOSOptions
273
+ {
274
+ ApiKey = "sk_test",
275
+ HttpClient = this.httpMock.HttpClient,
276
+ });
277
+ this.service = new OrganizationsService(client);
278
+ }
279
+
280
+ [Fact]
281
+ public async Task TestGetOrganization()
282
+ {
283
+ var fixture = File.ReadAllText("testdata/organization.json");
284
+ this.httpMock.MockResponse(HttpMethod.Get, "/organizations/org_01234", HttpStatusCode.OK, fixture);
285
+ var result = await this.service.GetOrganization("org_01234");
286
+ Assert.NotNull(result);
287
+ Assert.Equal("org_01234", result.Id);
288
+ this.httpMock.AssertRequestWasMade(HttpMethod.Get, "/organizations/org_01234");
289
+ }
290
+ }
291
+ ```
292
+
293
+ ## Structural Guidelines
294
+
295
+ | Category | Choice |
296
+ | ------------------ | --------------------------- |
297
+ | Target Framework | .NET 8.0 |
298
+ | HTTP Client | System.Net.Http.HttpClient |
299
+ | JSON Parsing | Newtonsoft.Json 13.x |
300
+ | Testing Framework | xUnit 2.x |
301
+ | HTTP Mocking | Moq 4.x (HttpClientHandler) |
302
+ | Linting/Formatting | StyleCop.Analyzers |
303
+ | Package Manager | NuGet |
304
+ | Build Tool | dotnet CLI / MSBuild |
305
+
306
+ ## Directory Structure
307
+
308
+ ```
309
+ src/WorkOS.net/
310
+ ├── WorkOS.cs # @oagen-ignore-file
311
+ ├── Client/ # @oagen-ignore-file (all)
312
+ │ ├── WorkOSClient.cs
313
+ │ ├── _interfaces/
314
+ │ └── Utilities/
315
+ ├── Services/
316
+ │ ├── _common/ # @oagen-ignore-file (all)
317
+ │ ├── Webhooks/ # Mixed hand-written + generated
318
+ │ │ ├── Entities/Webhook.cs # @oagen-ignore-file
319
+ │ │ ├── Exceptions/WorkOSWebhookException.cs
320
+ │ │ ├── WebhookService.cs # @oagen-ignore-file
321
+ │ │ └── WebhooksService.cs # Generated
322
+ │ └── {ServiceName}/ # Generated
323
+ │ ├── {ServiceName}Service.cs
324
+ │ ├── _interfaces/
325
+ │ │ └── {Action}{Entity}Options.cs
326
+ │ ├── Entities/
327
+ │ │ └── {Entity}.cs
328
+ │ └── Enums/
329
+ │ └── {EnumName}.cs
330
+ test/WorkOSTests/
331
+ ├── Utilities/HttpMock.cs # @oagen-ignore-file
332
+ ├── Client/ # @oagen-ignore-file
333
+ ├── Services/Webhooks/WebhookTests.cs # @oagen-ignore-file
334
+ ├── Tests/{ServiceName}Test.cs # Generated
335
+ └── xunit.runner.json # Generated
336
+ ```
package/oagen.config.ts CHANGED
@@ -4,6 +4,8 @@ import { nodeEmitter } from './src/node/index.js';
4
4
  import { pythonEmitter } from './src/python/index.js';
5
5
  import { phpEmitter } from './src/php/index.js';
6
6
  import { goEmitter } from './src/go/index.js';
7
+ import { dotnetEmitter } from './src/dotnet/index.js';
8
+ import { kotlinEmitter } from './src/kotlin/index.js';
7
9
  import { nodeExtractor } from './src/compat/extractors/node.js';
8
10
  import { rubyExtractor } from './src/compat/extractors/ruby.js';
9
11
  import { pythonExtractor } from './src/compat/extractors/python.js';
@@ -39,8 +41,9 @@ const operationHints: Record<string, OperationHint> = {
39
41
  name: 'get_authorization_url',
40
42
  defaults: { response_type: 'code' },
41
43
  inferFromClient: ['client_id'],
44
+ urlBuilder: true,
42
45
  },
43
- 'GET /sso/logout': { name: 'get_logout_url' },
46
+ 'GET /sso/logout': { name: 'get_logout_url', urlBuilder: true },
44
47
  'GET /sso/profile': { name: 'get_profile' },
45
48
  'POST /sso/token': {
46
49
  name: 'get_profile_and_token',
@@ -56,8 +59,9 @@ const operationHints: Record<string, OperationHint> = {
56
59
  name: 'get_authorization_url',
57
60
  defaults: { response_type: 'code' },
58
61
  inferFromClient: ['client_id'],
62
+ urlBuilder: true,
59
63
  },
60
- 'GET /user_management/sessions/logout': { name: 'get_logout_url' },
64
+ 'GET /user_management/sessions/logout': { name: 'get_logout_url', urlBuilder: true },
61
65
 
62
66
  // ── User Management — org membership actions ────────────────────────────
63
67
  'PUT /user_management/organization_memberships/{id}/deactivate': {
@@ -153,11 +157,22 @@ const operationHints: Record<string, OperationHint> = {
153
157
  name: 'remove_flag_target',
154
158
  },
155
159
 
160
+ // ── Organizations — audit log config (singular fetch, not a list) ───────
161
+ 'GET /organizations/{id}/audit_log_configuration': {
162
+ name: 'get_audit_log_configuration',
163
+ },
164
+
156
165
  // ── Organizations — audit logs retention (mounted on AuditLogs) ─────────
157
- 'GET /organizations/{id}/audit_logs_retention': { mountOn: 'AuditLogs' },
166
+ 'GET /organizations/{id}/audit_logs_retention': {
167
+ name: 'get_organization_audit_logs_retention',
168
+ mountOn: 'AuditLogs',
169
+ },
158
170
  'PUT /organizations/{id}/audit_logs_retention': { mountOn: 'AuditLogs' },
159
171
 
160
172
  // ── Union split: POST /user_management/authenticate (8 variants) ────────
173
+ // Common optional fields appended to every variant's exposedParams so the
174
+ // generated wrappers cover the full spec body shape (fraud/audit context the
175
+ // server consumes when present).
161
176
  'POST /user_management/authenticate': {
162
177
  split: [
163
178
  {
@@ -165,56 +180,71 @@ const operationHints: Record<string, OperationHint> = {
165
180
  targetVariant: 'PasswordSessionAuthenticateRequest',
166
181
  defaults: { grant_type: 'password' },
167
182
  inferFromClient: ['client_id', 'client_secret'],
168
- exposedParams: ['email', 'password', 'invitation_token'],
183
+ exposedParams: ['email', 'password', 'invitation_token', 'ip_address', 'device_id', 'user_agent'],
184
+ optionalParams: ['invitation_token', 'ip_address', 'device_id', 'user_agent'],
169
185
  },
170
186
  {
171
187
  name: 'authenticate_with_code',
172
188
  targetVariant: 'CodeSessionAuthenticateRequest',
173
189
  defaults: { grant_type: 'authorization_code' },
174
190
  inferFromClient: ['client_id', 'client_secret'],
175
- exposedParams: ['code'],
191
+ exposedParams: ['code', 'ip_address', 'device_id', 'user_agent'],
192
+ optionalParams: ['ip_address', 'device_id', 'user_agent'],
176
193
  },
177
194
  {
178
195
  name: 'authenticate_with_refresh_token',
179
196
  targetVariant: 'RefreshTokenSessionAuthenticateRequest',
180
197
  defaults: { grant_type: 'refresh_token' },
181
198
  inferFromClient: ['client_id', 'client_secret'],
182
- exposedParams: ['refresh_token', 'organization_id'],
199
+ exposedParams: ['refresh_token', 'organization_id', 'ip_address', 'device_id', 'user_agent'],
200
+ optionalParams: ['organization_id', 'ip_address', 'device_id', 'user_agent'],
183
201
  },
184
202
  {
185
203
  name: 'authenticate_with_magic_auth',
186
204
  targetVariant: 'MagicAuthSessionAuthenticateRequest',
187
205
  defaults: { grant_type: 'urn:workos:oauth:grant-type:magic-auth:code' },
188
206
  inferFromClient: ['client_id', 'client_secret'],
189
- exposedParams: ['code', 'email', 'invitation_token'],
207
+ exposedParams: ['code', 'email', 'invitation_token', 'ip_address', 'device_id', 'user_agent'],
208
+ optionalParams: ['invitation_token', 'ip_address', 'device_id', 'user_agent'],
190
209
  },
191
210
  {
192
211
  name: 'authenticate_with_email_verification',
193
212
  targetVariant: 'EmailVerificationSessionAuthenticateRequest',
194
213
  defaults: { grant_type: 'urn:workos:oauth:grant-type:email-verification:code' },
195
214
  inferFromClient: ['client_id', 'client_secret'],
196
- exposedParams: ['code', 'pending_authentication_token'],
215
+ exposedParams: ['code', 'pending_authentication_token', 'ip_address', 'device_id', 'user_agent'],
216
+ optionalParams: ['pending_authentication_token', 'ip_address', 'device_id', 'user_agent'],
197
217
  },
198
218
  {
199
219
  name: 'authenticate_with_totp',
200
220
  targetVariant: 'TotpSessionAuthenticateRequest',
201
221
  defaults: { grant_type: 'urn:workos:oauth:grant-type:mfa-totp' },
202
222
  inferFromClient: ['client_id', 'client_secret'],
203
- exposedParams: ['code', 'pending_authentication_token', 'authentication_challenge_id'],
223
+ exposedParams: [
224
+ 'code',
225
+ 'pending_authentication_token',
226
+ 'authentication_challenge_id',
227
+ 'ip_address',
228
+ 'device_id',
229
+ 'user_agent',
230
+ ],
231
+ optionalParams: ['ip_address', 'device_id', 'user_agent'],
204
232
  },
205
233
  {
206
234
  name: 'authenticate_with_organization_selection',
207
235
  targetVariant: 'OrganizationSelectionSessionAuthenticateRequest',
208
236
  defaults: { grant_type: 'urn:workos:oauth:grant-type:organization-selection' },
209
237
  inferFromClient: ['client_id', 'client_secret'],
210
- exposedParams: ['pending_authentication_token', 'organization_id'],
238
+ exposedParams: ['pending_authentication_token', 'organization_id', 'ip_address', 'device_id', 'user_agent'],
239
+ optionalParams: ['ip_address', 'device_id', 'user_agent'],
211
240
  },
212
241
  {
213
242
  name: 'authenticate_with_device_code',
214
243
  targetVariant: 'DeviceCodeSessionAuthenticateRequest',
215
244
  defaults: { grant_type: 'urn:ietf:params:oauth:grant-type:device_code' },
216
245
  inferFromClient: ['client_id'],
217
- exposedParams: ['device_code'],
246
+ exposedParams: ['device_code', 'ip_address', 'device_id', 'user_agent'],
247
+ optionalParams: ['ip_address', 'device_id', 'user_agent'],
218
248
  },
219
249
  ],
220
250
  },
@@ -299,7 +329,7 @@ const mountRules: Record<string, string> = {
299
329
  };
300
330
 
301
331
  const config: OagenConfig = {
302
- emitters: [nodeEmitter, pythonEmitter, phpEmitter, goEmitter],
332
+ emitters: [nodeEmitter, pythonEmitter, phpEmitter, goEmitter, dotnetEmitter, kotlinEmitter],
303
333
  extractors: [
304
334
  nodeExtractor,
305
335
  rubyExtractor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -78,6 +78,6 @@
78
78
  "node": ">=24.10.0"
79
79
  },
80
80
  "dependencies": {
81
- "@workos/oagen": "^0.5.0"
81
+ "@workos/oagen": "^0.6.0"
82
82
  }
83
83
  }
@@ -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
+ }