@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.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3288 -791
- package/dist/index.mjs.map +1 -1
- package/docs/sdk-architecture/dotnet.md +336 -0
- package/oagen.config.ts +42 -12
- package/package.json +2 -2
- package/smoke/sdk-dotnet.ts +45 -12
- package/src/dotnet/client.ts +89 -0
- package/src/dotnet/enums.ts +323 -0
- package/src/dotnet/fixtures.ts +236 -0
- package/src/dotnet/index.ts +246 -0
- package/src/dotnet/manifest.ts +36 -0
- package/src/dotnet/models.ts +344 -0
- package/src/dotnet/naming.ts +330 -0
- package/src/dotnet/resources.ts +622 -0
- package/src/dotnet/tests.ts +693 -0
- package/src/dotnet/type-map.ts +201 -0
- package/src/dotnet/wrappers.ts +186 -0
- package/src/go/index.ts +5 -2
- package/src/go/naming.ts +5 -17
- package/src/index.ts +1 -0
- package/src/kotlin/client.ts +53 -0
- package/src/kotlin/enums.ts +162 -0
- package/src/kotlin/index.ts +92 -0
- package/src/kotlin/manifest.ts +55 -0
- package/src/kotlin/models.ts +395 -0
- package/src/kotlin/naming.ts +223 -0
- package/src/kotlin/overrides.ts +25 -0
- package/src/kotlin/resources.ts +667 -0
- package/src/kotlin/tests.ts +1019 -0
- package/src/kotlin/type-map.ts +123 -0
- package/src/kotlin/wrappers.ts +168 -0
- package/src/node/client.ts +50 -0
- package/src/node/index.ts +1 -0
- package/src/node/resources.ts +164 -44
- package/src/node/tests.ts +37 -7
- package/src/php/client.ts +11 -3
- package/src/php/naming.ts +2 -21
- package/src/php/resources.ts +81 -6
- package/src/php/tests.ts +93 -17
- package/src/php/wrappers.ts +1 -0
- package/src/python/client.ts +37 -29
- package/src/python/enums.ts +7 -7
- package/src/python/models.ts +1 -1
- package/src/python/naming.ts +2 -22
- package/src/shared/model-utils.ts +232 -15
- package/src/shared/naming-utils.ts +47 -0
- package/src/shared/wrapper-utils.ts +12 -1
- package/test/dotnet/client.test.ts +121 -0
- package/test/dotnet/enums.test.ts +193 -0
- package/test/dotnet/errors.test.ts +9 -0
- package/test/dotnet/manifest.test.ts +82 -0
- package/test/dotnet/models.test.ts +260 -0
- package/test/dotnet/resources.test.ts +255 -0
- package/test/dotnet/tests.test.ts +202 -0
- package/test/kotlin/models.test.ts +135 -0
- package/test/kotlin/tests.test.ts +176 -0
- package/test/node/client.test.ts +74 -0
- package/test/node/resources.test.ts +216 -15
- package/test/php/client.test.ts +2 -1
- package/test/php/resources.test.ts +38 -0
- 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': {
|
|
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: [
|
|
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
|
+
"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.
|
|
81
|
+
"@workos/oagen": "^0.6.0"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/smoke/sdk-dotnet.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "${
|
|
626
|
-
cwd:
|
|
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
|
|
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
|
+
}
|