workos 0.7.3 → 0.8.1

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 (151) hide show
  1. package/README.md +353 -8
  2. package/dist/bin.js +938 -128
  3. package/dist/bin.js.map +1 -1
  4. package/dist/commands/api-key-mgmt.d.ts +16 -0
  5. package/dist/commands/api-key-mgmt.js +96 -0
  6. package/dist/commands/api-key-mgmt.js.map +1 -0
  7. package/dist/commands/audit-log.d.ts +26 -0
  8. package/dist/commands/audit-log.js +155 -0
  9. package/dist/commands/audit-log.js.map +1 -0
  10. package/dist/commands/config.d.ts +3 -0
  11. package/dist/commands/config.js +54 -0
  12. package/dist/commands/config.js.map +1 -0
  13. package/dist/commands/connection.d.ts +13 -0
  14. package/dist/commands/connection.js +94 -0
  15. package/dist/commands/connection.js.map +1 -0
  16. package/dist/commands/debug-sso.d.ts +1 -0
  17. package/dist/commands/debug-sso.js +78 -0
  18. package/dist/commands/debug-sso.js.map +1 -0
  19. package/dist/commands/debug-sync.d.ts +1 -0
  20. package/dist/commands/debug-sync.js +102 -0
  21. package/dist/commands/debug-sync.js.map +1 -0
  22. package/dist/commands/directory.d.ts +27 -0
  23. package/dist/commands/directory.js +174 -0
  24. package/dist/commands/directory.js.map +1 -0
  25. package/dist/commands/env.js +41 -28
  26. package/dist/commands/env.js.map +1 -1
  27. package/dist/commands/event.d.ts +9 -0
  28. package/dist/commands/event.js +43 -0
  29. package/dist/commands/event.js.map +1 -0
  30. package/dist/commands/feature-flag.d.ts +12 -0
  31. package/dist/commands/feature-flag.js +96 -0
  32. package/dist/commands/feature-flag.js.map +1 -0
  33. package/dist/commands/install-skill.js +3 -5
  34. package/dist/commands/install-skill.js.map +1 -1
  35. package/dist/commands/install.js +13 -20
  36. package/dist/commands/install.js.map +1 -1
  37. package/dist/commands/invitation.d.ts +19 -0
  38. package/dist/commands/invitation.js +94 -0
  39. package/dist/commands/invitation.js.map +1 -0
  40. package/dist/commands/membership.d.ts +20 -0
  41. package/dist/commands/membership.js +129 -0
  42. package/dist/commands/membership.js.map +1 -0
  43. package/dist/commands/onboard-user.d.ts +7 -0
  44. package/dist/commands/onboard-user.js +61 -0
  45. package/dist/commands/onboard-user.js.map +1 -0
  46. package/dist/commands/org-domain.d.ts +4 -0
  47. package/dist/commands/org-domain.js +45 -0
  48. package/dist/commands/org-domain.js.map +1 -0
  49. package/dist/commands/organization.d.ts +1 -5
  50. package/dist/commands/organization.js +34 -73
  51. package/dist/commands/organization.js.map +1 -1
  52. package/dist/commands/permission.d.ts +20 -0
  53. package/dist/commands/permission.js +93 -0
  54. package/dist/commands/permission.js.map +1 -0
  55. package/dist/commands/portal.d.ts +7 -0
  56. package/dist/commands/portal.js +26 -0
  57. package/dist/commands/portal.js.map +1 -0
  58. package/dist/commands/role.d.ts +17 -0
  59. package/dist/commands/role.js +122 -0
  60. package/dist/commands/role.js.map +1 -0
  61. package/dist/commands/seed.d.ts +4 -0
  62. package/dist/commands/seed.js +238 -0
  63. package/dist/commands/seed.js.map +1 -0
  64. package/dist/commands/session.d.ts +8 -0
  65. package/dist/commands/session.js +63 -0
  66. package/dist/commands/session.js.map +1 -0
  67. package/dist/commands/setup-org.d.ts +6 -0
  68. package/dist/commands/setup-org.js +99 -0
  69. package/dist/commands/setup-org.js.map +1 -0
  70. package/dist/commands/user.js +35 -71
  71. package/dist/commands/user.js.map +1 -1
  72. package/dist/commands/vault.d.ts +24 -0
  73. package/dist/commands/vault.js +120 -0
  74. package/dist/commands/vault.js.map +1 -0
  75. package/dist/commands/webhook.d.ts +3 -0
  76. package/dist/commands/webhook.js +73 -0
  77. package/dist/commands/webhook.js.map +1 -0
  78. package/dist/dashboard/components/DiffPanel.js.map +1 -1
  79. package/dist/dashboard/lib/logo-frames.js +1 -1
  80. package/dist/dashboard/lib/logo-frames.js.map +1 -1
  81. package/dist/doctor/checks/dashboard.js.map +1 -1
  82. package/dist/doctor/checks/environment.js.map +1 -1
  83. package/dist/integrations/go/index.js +1 -3
  84. package/dist/integrations/go/index.js.map +1 -1
  85. package/dist/lib/adapters/headless-adapter.d.ts +67 -0
  86. package/dist/lib/adapters/headless-adapter.js +263 -0
  87. package/dist/lib/adapters/headless-adapter.js.map +1 -0
  88. package/dist/lib/adapters/index.d.ts +1 -0
  89. package/dist/lib/adapters/index.js +1 -0
  90. package/dist/lib/adapters/index.js.map +1 -1
  91. package/dist/lib/agent-interface.d.ts +3 -11
  92. package/dist/lib/agent-interface.js +3 -19
  93. package/dist/lib/agent-interface.js.map +1 -1
  94. package/dist/lib/api-error-handler.d.ts +6 -0
  95. package/dist/lib/api-error-handler.js +58 -0
  96. package/dist/lib/api-error-handler.js.map +1 -0
  97. package/dist/lib/api-key.js +5 -1
  98. package/dist/lib/api-key.js.map +1 -1
  99. package/dist/lib/config.js.map +1 -1
  100. package/dist/lib/credential-proxy.js +0 -6
  101. package/dist/lib/credential-proxy.js.map +1 -1
  102. package/dist/lib/device-auth.js +1 -1
  103. package/dist/lib/device-auth.js.map +1 -1
  104. package/dist/lib/ensure-auth.js +25 -4
  105. package/dist/lib/ensure-auth.js.map +1 -1
  106. package/dist/lib/installer-core.d.ts +12 -12
  107. package/dist/lib/run-with-core.js +25 -4
  108. package/dist/lib/run-with-core.js.map +1 -1
  109. package/dist/lib/validation/validator.js +0 -1
  110. package/dist/lib/validation/validator.js.map +1 -1
  111. package/dist/lib/workos-client.d.ts +58 -0
  112. package/dist/lib/workos-client.js +137 -0
  113. package/dist/lib/workos-client.js.map +1 -0
  114. package/dist/run.d.ts +7 -0
  115. package/dist/run.js +5 -2
  116. package/dist/run.js.map +1 -1
  117. package/dist/smoke-test.ts +881 -0
  118. package/dist/steps/run-prettier.js +1 -1
  119. package/dist/steps/run-prettier.js.map +1 -1
  120. package/dist/utils/analytics.d.ts +1 -1
  121. package/dist/utils/analytics.js.map +1 -1
  122. package/dist/utils/clack-utils.js +1 -1
  123. package/dist/utils/clack-utils.js.map +1 -1
  124. package/dist/utils/environment.js +8 -0
  125. package/dist/utils/environment.js.map +1 -1
  126. package/dist/utils/exit-codes.d.ts +22 -0
  127. package/dist/utils/exit-codes.js +30 -0
  128. package/dist/utils/exit-codes.js.map +1 -0
  129. package/dist/utils/help-json.d.ts +45 -0
  130. package/dist/utils/help-json.js +1161 -0
  131. package/dist/utils/help-json.js.map +1 -0
  132. package/dist/utils/ndjson.d.ts +16 -0
  133. package/dist/utils/ndjson.js +18 -0
  134. package/dist/utils/ndjson.js.map +1 -0
  135. package/dist/utils/output.d.ts +40 -0
  136. package/dist/utils/output.js +95 -0
  137. package/dist/utils/output.js.map +1 -0
  138. package/dist/utils/package-manager.js +2 -3
  139. package/dist/utils/package-manager.js.map +1 -1
  140. package/dist/utils/paths.d.ts +5 -0
  141. package/dist/utils/paths.js +18 -0
  142. package/dist/utils/paths.js.map +1 -0
  143. package/dist/utils/register-subcommand.d.ts +7 -0
  144. package/dist/utils/register-subcommand.js +36 -0
  145. package/dist/utils/register-subcommand.js.map +1 -0
  146. package/dist/utils/telemetry-types.d.ts +1 -1
  147. package/dist/utils/telemetry-types.js.map +1 -1
  148. package/dist/utils/types.d.ts +12 -0
  149. package/dist/utils/types.js.map +1 -1
  150. package/package.json +20 -16
  151. package/skills/workos-management/SKILL.md +250 -0
@@ -0,0 +1,881 @@
1
+ /**
2
+ * Smoke test for CLI management commands.
3
+ *
4
+ * Exercises each command handler directly against the real WorkOS API
5
+ * to verify SDK method signatures are correct.
6
+ *
7
+ * Usage:
8
+ * WORKOS_API_KEY=sk_test_xxx pnpm tsx scripts/smoke-test.ts
9
+ */
10
+
11
+ import { writeFileSync, unlinkSync, existsSync } from 'node:fs';
12
+ import { setOutputMode } from '../src/utils/output.js';
13
+ import { createWorkOSClient } from '../src/lib/workos-client.js';
14
+
15
+ setOutputMode('json');
16
+
17
+ // Intercept process.exit so handler errors (exitWithError) don't kill the smoke test
18
+ const realExit = process.exit;
19
+ let lastExitCode: number | undefined;
20
+ process.exit = ((code?: number) => {
21
+ lastExitCode = code ?? 0;
22
+ throw new Error(`process.exit(${code}) intercepted`);
23
+ }) as never;
24
+
25
+ const apiKey = process.env.WORKOS_API_KEY;
26
+ if (!apiKey) {
27
+ realExit.call(process, 1);
28
+ }
29
+
30
+ interface TestResult {
31
+ name: string;
32
+ status: 'pass' | 'fail' | 'skip';
33
+ error?: string;
34
+ duration?: number;
35
+ }
36
+
37
+ const results: TestResult[] = [];
38
+
39
+ // Captured output from handlers (we parse this to extract IDs)
40
+ let capturedOutput: string[] = [];
41
+
42
+ async function test(name: string, fn: () => Promise<void>): Promise<void> {
43
+ const start = Date.now();
44
+ capturedOutput = [];
45
+ lastExitCode = undefined;
46
+ try {
47
+ await fn();
48
+ results.push({ name, status: 'pass', duration: Date.now() - start });
49
+ process.stdout.write(` ✓ ${name} (${Date.now() - start}ms)\n`);
50
+ } catch (error: unknown) {
51
+ const msg = error instanceof Error ? error.message : String(error);
52
+ const stack = error instanceof Error ? error.stack : undefined;
53
+
54
+ // Auth errors = signature is correct, key just lacks access
55
+ if (msg.includes('401') || msg.includes('403') || msg.includes('Unauthorized') || msg.includes('Forbidden')) {
56
+ results.push({ name, status: 'pass', duration: Date.now() - start });
57
+ process.stdout.write(` ✓ ${name} (auth-limited, signature OK) (${Date.now() - start}ms)\n`);
58
+ return;
59
+ }
60
+
61
+ // Export timeout = signature is correct, just slow
62
+ if (msg.includes('Export timed out')) {
63
+ results.push({ name, status: 'pass', duration: Date.now() - start });
64
+ process.stdout.write(` ✓ ${name} (timed out, signature OK) (${Date.now() - start}ms)\n`);
65
+ return;
66
+ }
67
+
68
+ // Structured API errors (400, 404, 422) = call reached the API, signature is correct,
69
+ // business logic rejected it (missing config, entity not found, validation, etc.)
70
+ if (
71
+ msg.includes('process.exit') &&
72
+ capturedOutput.some((o) => {
73
+ try {
74
+ const p = JSON.parse(o.replace('[stderr] ', ''));
75
+ return p?.error?.code;
76
+ } catch {
77
+ return false;
78
+ }
79
+ })
80
+ ) {
81
+ results.push({ name, status: 'pass', duration: Date.now() - start });
82
+ const apiErr = capturedOutput.find((o) => o.includes('"error"'));
83
+ const code = apiErr ? JSON.parse(apiErr.replace('[stderr] ', '')).error?.code : 'unknown';
84
+ process.stdout.write(` ✓ ${name} (api-rejected: ${code}, signature OK) (${Date.now() - start}ms)\n`);
85
+ return;
86
+ }
87
+
88
+ // Build detailed error message
89
+ const details: string[] = [` ✗ ${name}: ${msg}`];
90
+ if (lastExitCode !== undefined) {
91
+ details.push(` exit code: ${lastExitCode}`);
92
+ }
93
+ if (capturedOutput.length > 0) {
94
+ details.push(` handler output: ${capturedOutput.join(' | ')}`);
95
+ }
96
+ if (stack && !msg.includes('process.exit')) {
97
+ // Show a couple frames for real errors (not the exit intercept)
98
+ const frames = stack
99
+ .split('\n')
100
+ .slice(1, 4)
101
+ .map((l) => ` ${l.trim()}`);
102
+ details.push(...frames);
103
+ }
104
+
105
+ const fullError = details.join('\n');
106
+ results.push({ name, status: 'fail', error: fullError, duration: Date.now() - start });
107
+ process.stdout.write(fullError + '\n');
108
+ }
109
+ }
110
+
111
+ /** Parse the first captured JSON output line */
112
+ function parseOutput(): unknown {
113
+ for (const line of capturedOutput) {
114
+ try {
115
+ return JSON.parse(line);
116
+ } catch {
117
+ continue;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ // Suppress console.log/error from handlers, capture output
124
+ const origLog = console.log;
125
+ const origError = console.error;
126
+ function muteConsole() {
127
+ console.log = (...args: unknown[]) => {
128
+ capturedOutput.push(args.map(String).join(' '));
129
+ };
130
+ console.error = (...args: unknown[]) => {
131
+ capturedOutput.push('[stderr] ' + args.map(String).join(' '));
132
+ };
133
+ }
134
+ function unmuteConsole() {
135
+ console.log = origLog;
136
+ console.error = origError;
137
+ }
138
+
139
+ // Cleanup registry — functions to call at the end
140
+ const cleanups: Array<() => Promise<void>> = [];
141
+
142
+ function section(name: string) {
143
+ unmuteConsole();
144
+ process.stdout.write(`\n${name}:\n`);
145
+ muteConsole();
146
+ }
147
+
148
+ async function run() {
149
+ process.stdout.write('\n🔍 WorkOS CLI Smoke Test\n');
150
+ process.stdout.write(` API Key: ${apiKey!.substring(0, 12)}...\n\n`);
151
+
152
+ const client = createWorkOSClient(apiKey);
153
+
154
+ // ---- Setup: create test org for commands that need an org ID ----
155
+ process.stdout.write('Setup:\n');
156
+ const testOrgName = `smoke-test-${Date.now()}`;
157
+ let testOrgId: string | undefined;
158
+ let testUserId: string | undefined;
159
+
160
+ try {
161
+ const org = await client.sdk.organizations.createOrganization({ name: testOrgName });
162
+ testOrgId = org.id;
163
+ process.stdout.write(` Created test org: ${testOrgId}\n`);
164
+ cleanups.push(async () => {
165
+ await client.sdk.organizations.deleteOrganization(testOrgId!);
166
+ process.stdout.write(` Cleaned up org: ${testOrgId}\n`);
167
+ });
168
+ } catch (e) {
169
+ process.stdout.write(` ⚠ Could not create test org: ${e instanceof Error ? e.message : e}\n`);
170
+ }
171
+
172
+ // Get a user ID from existing users
173
+ try {
174
+ const users = await client.sdk.userManagement.listUsers({ limit: 1 });
175
+ if (users.data.length > 0) {
176
+ testUserId = users.data[0].id;
177
+ process.stdout.write(` Found test user: ${testUserId}\n`);
178
+ }
179
+ } catch {
180
+ process.stdout.write(` ⚠ Could not list users for test user ID\n`);
181
+ }
182
+
183
+ process.stdout.write('\n');
184
+
185
+ // =====================================================================
186
+ // Organization (lifecycle)
187
+ // =====================================================================
188
+ section('Organization');
189
+ await test('organization list', async () => {
190
+ const { runOrgList } = await import('../src/commands/organization.js');
191
+ await runOrgList({}, apiKey!);
192
+ });
193
+
194
+ const orgLifecycleName = `smoke-org-lifecycle-${Date.now()}`;
195
+ let lifecycleOrgId: string | undefined;
196
+ await test('organization create', async () => {
197
+ const { runOrgCreate } = await import('../src/commands/organization.js');
198
+ await runOrgCreate(orgLifecycleName, [], apiKey!);
199
+ const output = parseOutput() as { data?: { id?: string } } | null;
200
+ lifecycleOrgId = output?.data?.id;
201
+ });
202
+ if (lifecycleOrgId) {
203
+ await test('organization get', async () => {
204
+ const { runOrgGet } = await import('../src/commands/organization.js');
205
+ await runOrgGet(lifecycleOrgId!, apiKey!);
206
+ });
207
+ await test('organization update', async () => {
208
+ const { runOrgUpdate } = await import('../src/commands/organization.js');
209
+ await runOrgUpdate(lifecycleOrgId!, `${orgLifecycleName}-updated`, apiKey!);
210
+ });
211
+ await test('organization delete', async () => {
212
+ const { runOrgDelete } = await import('../src/commands/organization.js');
213
+ await runOrgDelete(lifecycleOrgId!, apiKey!);
214
+ });
215
+ }
216
+
217
+ // =====================================================================
218
+ // User (read + update — no create/delete for safety)
219
+ // =====================================================================
220
+ section('User');
221
+ await test('user list', async () => {
222
+ const { runUserList } = await import('../src/commands/user.js');
223
+ await runUserList({}, apiKey!);
224
+ });
225
+ if (testUserId) {
226
+ await test('user get', async () => {
227
+ const { runUserGet } = await import('../src/commands/user.js');
228
+ await runUserGet(testUserId!, apiKey!);
229
+ });
230
+ await test('user update', async () => {
231
+ const { runUserUpdate } = await import('../src/commands/user.js');
232
+ await runUserUpdate(testUserId!, apiKey!, {});
233
+ });
234
+ }
235
+
236
+ // =====================================================================
237
+ // Permission (full CRUD lifecycle)
238
+ // =====================================================================
239
+ section('Permission (lifecycle)');
240
+
241
+ const testPermSlug = `smoke-perm-${Date.now()}`;
242
+ const testPermSlug2 = `smoke-perm2-${Date.now()}`;
243
+ await test('permission create', async () => {
244
+ const { runPermissionCreate } = await import('../src/commands/permission.js');
245
+ await runPermissionCreate({ slug: testPermSlug, name: `Smoke Test ${testPermSlug}` }, apiKey!);
246
+ });
247
+ await test('permission create (second)', async () => {
248
+ const { runPermissionCreate } = await import('../src/commands/permission.js');
249
+ await runPermissionCreate({ slug: testPermSlug2, name: `Smoke Test ${testPermSlug2}` }, apiKey!);
250
+ });
251
+ await test('permission list', async () => {
252
+ const { runPermissionList } = await import('../src/commands/permission.js');
253
+ await runPermissionList({}, apiKey!);
254
+ });
255
+ await test('permission get', async () => {
256
+ const { runPermissionGet } = await import('../src/commands/permission.js');
257
+ await runPermissionGet(testPermSlug, apiKey!);
258
+ });
259
+ await test('permission update', async () => {
260
+ const { runPermissionUpdate } = await import('../src/commands/permission.js');
261
+ await runPermissionUpdate(testPermSlug, { name: `Updated ${testPermSlug}` }, apiKey!);
262
+ });
263
+ // Cleanup permissions after role tests use them
264
+ cleanups.push(async () => {
265
+ try {
266
+ const { runPermissionDelete } = await import('../src/commands/permission.js');
267
+ muteConsole();
268
+ await runPermissionDelete(testPermSlug, apiKey!);
269
+ await runPermissionDelete(testPermSlug2, apiKey!);
270
+ unmuteConsole();
271
+ process.stdout.write(` Cleaned up permissions: ${testPermSlug}, ${testPermSlug2}\n`);
272
+ } catch {}
273
+ });
274
+
275
+ // =====================================================================
276
+ // Role (full CRUD lifecycle + permission ops, org-scoped)
277
+ // =====================================================================
278
+ section('Role (lifecycle)');
279
+ await test('role list (env)', async () => {
280
+ const { runRoleList } = await import('../src/commands/role.js');
281
+ await runRoleList(undefined, apiKey!);
282
+ });
283
+ if (testOrgId) {
284
+ await test('role list (org)', async () => {
285
+ const { runRoleList } = await import('../src/commands/role.js');
286
+ await runRoleList(testOrgId, apiKey!);
287
+ });
288
+
289
+ const testRoleSlug = `org-smoke-role-${Date.now()}`;
290
+ await test('role create (org)', async () => {
291
+ const { runRoleCreate } = await import('../src/commands/role.js');
292
+ await runRoleCreate({ slug: testRoleSlug, name: `Smoke Role ${testRoleSlug}` }, testOrgId, apiKey!);
293
+ });
294
+ await test('role get (org)', async () => {
295
+ const { runRoleGet } = await import('../src/commands/role.js');
296
+ await runRoleGet(testRoleSlug, testOrgId, apiKey!);
297
+ });
298
+ await test('role update (org)', async () => {
299
+ const { runRoleUpdate } = await import('../src/commands/role.js');
300
+ await runRoleUpdate(testRoleSlug, { name: `Updated ${testRoleSlug}` }, testOrgId, apiKey!);
301
+ });
302
+ await test('role set-permissions', async () => {
303
+ const { runRoleSetPermissions } = await import('../src/commands/role.js');
304
+ await runRoleSetPermissions(testRoleSlug, [testPermSlug], testOrgId, apiKey!);
305
+ });
306
+ await test('role add-permission', async () => {
307
+ const { runRoleAddPermission } = await import('../src/commands/role.js');
308
+ await runRoleAddPermission(testRoleSlug, testPermSlug2, testOrgId, apiKey!);
309
+ });
310
+ await test('role remove-permission', async () => {
311
+ const { runRoleRemovePermission } = await import('../src/commands/role.js');
312
+ await runRoleRemovePermission(testRoleSlug, testPermSlug2, testOrgId!, apiKey!);
313
+ });
314
+ await test('role delete (org)', async () => {
315
+ const { runRoleDelete } = await import('../src/commands/role.js');
316
+ await runRoleDelete(testRoleSlug, testOrgId!, apiKey!);
317
+ });
318
+ }
319
+
320
+ // =====================================================================
321
+ // Membership (full lifecycle — needs org + user)
322
+ // =====================================================================
323
+ section('Membership (lifecycle)');
324
+ if (testOrgId) {
325
+ await test('membership list (by org)', async () => {
326
+ const { runMembershipList } = await import('../src/commands/membership.js');
327
+ await runMembershipList({ org: testOrgId }, apiKey!);
328
+ });
329
+ }
330
+ if (testUserId) {
331
+ await test('membership list (by user)', async () => {
332
+ const { runMembershipList } = await import('../src/commands/membership.js');
333
+ await runMembershipList({ user: testUserId }, apiKey!);
334
+ });
335
+ }
336
+ if (testOrgId && testUserId) {
337
+ let membershipId: string | undefined;
338
+ await test('membership create', async () => {
339
+ const { runMembershipCreate } = await import('../src/commands/membership.js');
340
+ await runMembershipCreate({ org: testOrgId!, user: testUserId! }, apiKey!);
341
+ const output = parseOutput() as { data?: { id?: string } } | null;
342
+ membershipId = output?.data?.id;
343
+ });
344
+ if (membershipId) {
345
+ await test('membership get', async () => {
346
+ const { runMembershipGet } = await import('../src/commands/membership.js');
347
+ await runMembershipGet(membershipId!, apiKey!);
348
+ });
349
+ await test('membership update', async () => {
350
+ const { runMembershipUpdate } = await import('../src/commands/membership.js');
351
+ await runMembershipUpdate(membershipId!, undefined, apiKey!);
352
+ });
353
+ await test('membership deactivate', async () => {
354
+ const { runMembershipDeactivate } = await import('../src/commands/membership.js');
355
+ await runMembershipDeactivate(membershipId!, apiKey!);
356
+ });
357
+ await test('membership reactivate', async () => {
358
+ const { runMembershipReactivate } = await import('../src/commands/membership.js');
359
+ await runMembershipReactivate(membershipId!, apiKey!);
360
+ });
361
+ await test('membership delete', async () => {
362
+ const { runMembershipDelete } = await import('../src/commands/membership.js');
363
+ await runMembershipDelete(membershipId!, apiKey!);
364
+ });
365
+ }
366
+ }
367
+
368
+ // =====================================================================
369
+ // Invitation (full lifecycle)
370
+ // =====================================================================
371
+ section('Invitation (lifecycle)');
372
+ await test('invitation list', async () => {
373
+ const { runInvitationList } = await import('../src/commands/invitation.js');
374
+ await runInvitationList({}, apiKey!);
375
+ });
376
+ if (testOrgId) {
377
+ let invId: string | undefined;
378
+ const invEmail = `smoke-inv-${Date.now()}@example.com`;
379
+ await test('invitation send', async () => {
380
+ const { runInvitationSend } = await import('../src/commands/invitation.js');
381
+ await runInvitationSend({ email: invEmail, org: testOrgId! }, apiKey!);
382
+ const output = parseOutput() as { data?: { id?: string } } | null;
383
+ invId = output?.data?.id;
384
+ });
385
+ if (invId) {
386
+ await test('invitation get', async () => {
387
+ const { runInvitationGet } = await import('../src/commands/invitation.js');
388
+ await runInvitationGet(invId!, apiKey!);
389
+ });
390
+ await test('invitation resend', async () => {
391
+ const { runInvitationResend } = await import('../src/commands/invitation.js');
392
+ await runInvitationResend(invId!, apiKey!);
393
+ });
394
+ await test('invitation revoke', async () => {
395
+ const { runInvitationRevoke } = await import('../src/commands/invitation.js');
396
+ await runInvitationRevoke(invId!, apiKey!);
397
+ });
398
+ }
399
+ }
400
+
401
+ // =====================================================================
402
+ // Session
403
+ // =====================================================================
404
+ section('Session');
405
+ if (testUserId) {
406
+ let sessionId: string | undefined;
407
+ await test('session list', async () => {
408
+ const { runSessionList } = await import('../src/commands/session.js');
409
+ await runSessionList(testUserId!, {}, apiKey!);
410
+ const output = parseOutput() as { data?: Array<{ id?: string }> } | null;
411
+ sessionId = output?.data?.[0]?.id;
412
+ });
413
+ if (sessionId) {
414
+ await test('session revoke', async () => {
415
+ const { runSessionRevoke } = await import('../src/commands/session.js');
416
+ await runSessionRevoke(sessionId!, apiKey!);
417
+ });
418
+ }
419
+ }
420
+
421
+ // =====================================================================
422
+ // Connection (read-only — delete is too destructive)
423
+ // =====================================================================
424
+ section('Connection');
425
+ await test('connection list', async () => {
426
+ const { runConnectionList } = await import('../src/commands/connection.js');
427
+ await runConnectionList({}, apiKey!);
428
+ });
429
+ try {
430
+ const connections = await client.sdk.sso.listConnections({ limit: 1 });
431
+ if (connections.data.length > 0) {
432
+ const connId = connections.data[0].id;
433
+ await test('connection get', async () => {
434
+ const { runConnectionGet } = await import('../src/commands/connection.js');
435
+ await runConnectionGet(connId, apiKey!);
436
+ });
437
+ }
438
+ } catch {}
439
+
440
+ // =====================================================================
441
+ // Directory (read-only + list-users/list-groups — delete too destructive)
442
+ // =====================================================================
443
+ section('Directory');
444
+ await test('directory list', async () => {
445
+ const { runDirectoryList } = await import('../src/commands/directory.js');
446
+ await runDirectoryList({}, apiKey!);
447
+ });
448
+ try {
449
+ const directories = await client.sdk.directorySync.listDirectories({ limit: 1 });
450
+ if (directories.data.length > 0) {
451
+ const dirId = directories.data[0].id;
452
+ await test('directory get', async () => {
453
+ const { runDirectoryGet } = await import('../src/commands/directory.js');
454
+ await runDirectoryGet(dirId, apiKey!);
455
+ });
456
+ await test('directory list-users', async () => {
457
+ const { runDirectoryListUsers } = await import('../src/commands/directory.js');
458
+ await runDirectoryListUsers({ directory: dirId }, apiKey!);
459
+ });
460
+ await test('directory list-groups', async () => {
461
+ const { runDirectoryListGroups } = await import('../src/commands/directory.js');
462
+ await runDirectoryListGroups({ directory: dirId }, apiKey!);
463
+ });
464
+ }
465
+ } catch {}
466
+
467
+ // =====================================================================
468
+ // Event
469
+ // =====================================================================
470
+ section('Event');
471
+ await test('event list', async () => {
472
+ const { runEventList } = await import('../src/commands/event.js');
473
+ await runEventList({ events: ['authentication.email_verification_succeeded'] }, apiKey!);
474
+ });
475
+
476
+ // =====================================================================
477
+ // Audit Log
478
+ // =====================================================================
479
+ section('Audit Log');
480
+ await test('audit-log list-actions', async () => {
481
+ const { runAuditLogListActions } = await import('../src/commands/audit-log.js');
482
+ await runAuditLogListActions(apiKey!);
483
+ });
484
+ if (testOrgId) {
485
+ await test('audit-log create-event', async () => {
486
+ const { runAuditLogCreateEvent } = await import('../src/commands/audit-log.js');
487
+ await runAuditLogCreateEvent(
488
+ testOrgId!,
489
+ {
490
+ action: 'smoke.test',
491
+ actorType: 'user',
492
+ actorId: 'smoke-test-actor',
493
+ actorName: 'Smoke Test',
494
+ },
495
+ apiKey!,
496
+ );
497
+ });
498
+ await test('audit-log export', async () => {
499
+ const { runAuditLogExport } = await import('../src/commands/audit-log.js');
500
+ const now = new Date();
501
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
502
+ await runAuditLogExport(
503
+ {
504
+ organizationId: testOrgId!,
505
+ rangeStart: yesterday.toISOString(),
506
+ rangeEnd: now.toISOString(),
507
+ },
508
+ apiKey!,
509
+ );
510
+ });
511
+ await test('audit-log get-retention', async () => {
512
+ const { runAuditLogGetRetention } = await import('../src/commands/audit-log.js');
513
+ await runAuditLogGetRetention(testOrgId!, apiKey!);
514
+ });
515
+ }
516
+ await test('audit-log get-schema', async () => {
517
+ const { runAuditLogGetSchema } = await import('../src/commands/audit-log.js');
518
+ await runAuditLogGetSchema('user.signed_in', apiKey!);
519
+ });
520
+ const schemaFile = `/tmp/smoke-audit-schema-${Date.now()}.json`;
521
+ const schemaAction = `smoke.test.${Date.now()}`;
522
+ writeFileSync(
523
+ schemaFile,
524
+ JSON.stringify({
525
+ targets: [{ type: 'user' }],
526
+ actor: { metadata: {} },
527
+ metadata: {},
528
+ }),
529
+ );
530
+ await test('audit-log create-schema', async () => {
531
+ const { runAuditLogCreateSchema } = await import('../src/commands/audit-log.js');
532
+ await runAuditLogCreateSchema(schemaAction, schemaFile, apiKey!);
533
+ });
534
+ try {
535
+ unlinkSync(schemaFile);
536
+ } catch {}
537
+
538
+ // =====================================================================
539
+ // Feature Flag (read + toggle lifecycle)
540
+ // =====================================================================
541
+ section('Feature Flag');
542
+ let ffSlug: string | undefined;
543
+ await test('feature-flag list', async () => {
544
+ const { runFeatureFlagList } = await import('../src/commands/feature-flag.js');
545
+ await runFeatureFlagList({}, apiKey!);
546
+ const output = parseOutput() as { data?: Array<{ key?: string }> } | null;
547
+ ffSlug = output?.data?.[0]?.key;
548
+ });
549
+ if (ffSlug) {
550
+ await test('feature-flag get', async () => {
551
+ const { runFeatureFlagGet } = await import('../src/commands/feature-flag.js');
552
+ await runFeatureFlagGet(ffSlug!, apiKey!);
553
+ });
554
+ await test('feature-flag disable', async () => {
555
+ const { runFeatureFlagDisable } = await import('../src/commands/feature-flag.js');
556
+ await runFeatureFlagDisable(ffSlug!, apiKey!);
557
+ });
558
+ await test('feature-flag enable', async () => {
559
+ const { runFeatureFlagEnable } = await import('../src/commands/feature-flag.js');
560
+ await runFeatureFlagEnable(ffSlug!, apiKey!);
561
+ });
562
+ await test('feature-flag add-target', async () => {
563
+ const { runFeatureFlagAddTarget } = await import('../src/commands/feature-flag.js');
564
+ await runFeatureFlagAddTarget(ffSlug!, `smoke-target-${Date.now()}`, apiKey!);
565
+ });
566
+ await test('feature-flag remove-target', async () => {
567
+ const { runFeatureFlagRemoveTarget } = await import('../src/commands/feature-flag.js');
568
+ await runFeatureFlagRemoveTarget(ffSlug!, `smoke-target-${Date.now()}`, apiKey!);
569
+ });
570
+ }
571
+
572
+ // =====================================================================
573
+ // Webhook (lifecycle)
574
+ // =====================================================================
575
+ section('Webhook (lifecycle)');
576
+ await test('webhook list', async () => {
577
+ const { runWebhookList } = await import('../src/commands/webhook.js');
578
+ await runWebhookList(apiKey!);
579
+ });
580
+ let webhookId: string | undefined;
581
+ await test('webhook create', async () => {
582
+ const { runWebhookCreate } = await import('../src/commands/webhook.js');
583
+ await runWebhookCreate(`https://smoke-test-${Date.now()}.example.com/webhook`, ['user.created'], apiKey!);
584
+ const output = parseOutput() as { data?: { id?: string } } | null;
585
+ webhookId = output?.data?.id;
586
+ });
587
+ if (webhookId) {
588
+ await test('webhook delete', async () => {
589
+ const { runWebhookDelete } = await import('../src/commands/webhook.js');
590
+ await runWebhookDelete(webhookId!, apiKey!);
591
+ });
592
+ }
593
+
594
+ // =====================================================================
595
+ // Config (write operations — idempotent)
596
+ // =====================================================================
597
+ section('Config');
598
+ await test('config redirect add', async () => {
599
+ const { runConfigRedirectAdd } = await import('../src/commands/config.js');
600
+ await runConfigRedirectAdd('http://localhost:19876/smoke-test-callback', apiKey!);
601
+ });
602
+ await test('config cors add', async () => {
603
+ const { runConfigCorsAdd } = await import('../src/commands/config.js');
604
+ await runConfigCorsAdd('http://localhost:19876', apiKey!);
605
+ });
606
+ await test('config homepage-url set', async () => {
607
+ const { runConfigHomepageUrlSet } = await import('../src/commands/config.js');
608
+ await runConfigHomepageUrlSet('http://localhost:3000', apiKey!);
609
+ });
610
+
611
+ // =====================================================================
612
+ // Portal
613
+ // =====================================================================
614
+ section('Portal');
615
+ if (testOrgId) {
616
+ await test('portal generate-link', async () => {
617
+ const { runPortalGenerateLink } = await import('../src/commands/portal.js');
618
+ await runPortalGenerateLink({ intent: 'sso', organization: testOrgId! }, apiKey!);
619
+ });
620
+ }
621
+
622
+ // =====================================================================
623
+ // Vault (full lifecycle)
624
+ // =====================================================================
625
+ section('Vault (lifecycle)');
626
+ await test('vault list', async () => {
627
+ const { runVaultList } = await import('../src/commands/vault.js');
628
+ await runVaultList({}, apiKey!);
629
+ });
630
+ const vaultName = `smoke-vault-${Date.now()}`;
631
+ let vaultId: string | undefined;
632
+
633
+ await test('vault create', async () => {
634
+ const { runVaultCreate } = await import('../src/commands/vault.js');
635
+ await runVaultCreate({ name: vaultName, value: 'smoke-test-secret', org: testOrgId }, apiKey!);
636
+ const output = parseOutput() as { data?: { id?: string } } | null;
637
+ vaultId = output?.data?.id;
638
+ });
639
+ if (vaultId) {
640
+ await test('vault get', async () => {
641
+ const { runVaultGet } = await import('../src/commands/vault.js');
642
+ await runVaultGet(vaultId!, apiKey!);
643
+ });
644
+ await test('vault get-by-name', async () => {
645
+ const { runVaultGetByName } = await import('../src/commands/vault.js');
646
+ await runVaultGetByName(vaultName, apiKey!);
647
+ });
648
+ await test('vault describe', async () => {
649
+ const { runVaultDescribe } = await import('../src/commands/vault.js');
650
+ await runVaultDescribe(vaultId!, apiKey!);
651
+ });
652
+ await test('vault update', async () => {
653
+ const { runVaultUpdate } = await import('../src/commands/vault.js');
654
+ await runVaultUpdate({ id: vaultId!, value: 'updated-secret' }, apiKey!);
655
+ });
656
+ await test('vault list-versions', async () => {
657
+ const { runVaultListVersions } = await import('../src/commands/vault.js');
658
+ await runVaultListVersions(vaultId!, apiKey!);
659
+ });
660
+ await test('vault delete', async () => {
661
+ const { runVaultDelete } = await import('../src/commands/vault.js');
662
+ await runVaultDelete(vaultId!, apiKey!);
663
+ });
664
+ }
665
+
666
+ // =====================================================================
667
+ // API Key (lifecycle)
668
+ // =====================================================================
669
+ section('API Key (lifecycle)');
670
+ if (testOrgId) {
671
+ await test('api-key list', async () => {
672
+ const { runApiKeyList } = await import('../src/commands/api-key-mgmt.js');
673
+ await runApiKeyList({ organizationId: testOrgId! }, apiKey!);
674
+ });
675
+ let apiKeyId: string | undefined;
676
+ let apiKeyValue: string | undefined;
677
+ await test('api-key create', async () => {
678
+ const { runApiKeyCreate } = await import('../src/commands/api-key-mgmt.js');
679
+ await runApiKeyCreate({ organizationId: testOrgId!, name: `smoke-key-${Date.now()}` }, apiKey!);
680
+ const output = parseOutput() as { data?: { id?: string; key?: string } } | null;
681
+ apiKeyId = output?.data?.id;
682
+ apiKeyValue = output?.data?.key;
683
+ });
684
+ if (apiKeyValue) {
685
+ await test('api-key validate', async () => {
686
+ const { runApiKeyValidate } = await import('../src/commands/api-key-mgmt.js');
687
+ await runApiKeyValidate(apiKeyValue!, apiKey!);
688
+ });
689
+ }
690
+ if (apiKeyId) {
691
+ await test('api-key delete', async () => {
692
+ const { runApiKeyDelete } = await import('../src/commands/api-key-mgmt.js');
693
+ await runApiKeyDelete(apiKeyId!, apiKey!);
694
+ });
695
+ }
696
+ }
697
+
698
+ // =====================================================================
699
+ // Org Domain (lifecycle: create → get → verify → delete)
700
+ // =====================================================================
701
+ section('Org Domain (lifecycle)');
702
+ if (testOrgId) {
703
+ let domainId: string | undefined;
704
+ await test('org-domain create', async () => {
705
+ const { runOrgDomainCreate } = await import('../src/commands/org-domain.js');
706
+ await runOrgDomainCreate(`smoke-${Date.now()}.test`, testOrgId!, apiKey!);
707
+ const output = parseOutput() as { data?: { id?: string } } | null;
708
+ domainId = output?.data?.id;
709
+ });
710
+ if (domainId) {
711
+ await test('org-domain get', async () => {
712
+ const { runOrgDomainGet } = await import('../src/commands/org-domain.js');
713
+ await runOrgDomainGet(domainId!, apiKey!);
714
+ });
715
+ await test('org-domain verify', async () => {
716
+ const { runOrgDomainVerify } = await import('../src/commands/org-domain.js');
717
+ await runOrgDomainVerify(domainId!, apiKey!);
718
+ });
719
+ await test('org-domain delete', async () => {
720
+ const { runOrgDomainDelete } = await import('../src/commands/org-domain.js');
721
+ await runOrgDomainDelete(domainId!, apiKey!);
722
+ });
723
+ }
724
+ }
725
+
726
+ // =====================================================================
727
+ // Seed (write temp YAML, run, clean)
728
+ // =====================================================================
729
+ section('Seed');
730
+ const seedFile = `/tmp/smoke-seed-${Date.now()}.yml`;
731
+ writeFileSync(
732
+ seedFile,
733
+ `
734
+ permissions:
735
+ - name: Smoke Read
736
+ slug: smoke-seed-read-${Date.now()}
737
+ roles:
738
+ - name: Smoke Viewer
739
+ slug: smoke-seed-viewer-${Date.now()}
740
+ `,
741
+ );
742
+ await test('seed (apply)', async () => {
743
+ const { runSeed } = await import('../src/commands/seed.js');
744
+ await runSeed({ file: seedFile }, apiKey!);
745
+ });
746
+ await test('seed (clean)', async () => {
747
+ const { runSeed } = await import('../src/commands/seed.js');
748
+ await runSeed({ clean: true }, apiKey!);
749
+ });
750
+ // Clean up temp files
751
+ try {
752
+ unlinkSync(seedFile);
753
+ } catch {}
754
+ try {
755
+ if (existsSync('.workos-seed-state.json')) unlinkSync('.workos-seed-state.json');
756
+ } catch {}
757
+
758
+ // =====================================================================
759
+ // Compound Workflows
760
+ // =====================================================================
761
+
762
+ // setup-org: creates org + domain + roles + portal link
763
+ section('Setup Org (workflow)');
764
+ const setupOrgName = `smoke-setup-${Date.now()}`;
765
+ await test('setup-org (name + domain + roles)', async () => {
766
+ const { runSetupOrg } = await import('../src/commands/setup-org.js');
767
+ await runSetupOrg({ name: setupOrgName, domain: `${setupOrgName}.test`, roles: ['admin', 'viewer'] }, apiKey!);
768
+ });
769
+ // Clean up the setup-org's created org
770
+ try {
771
+ const orgs = await client.sdk.organizations.listOrganizations({ limit: 5 });
772
+ const setupOrg = orgs.data.find((o) => o.name === setupOrgName);
773
+ if (setupOrg) {
774
+ cleanups.push(async () => {
775
+ await client.sdk.organizations.deleteOrganization(setupOrg.id);
776
+ process.stdout.write(` Cleaned up setup-org: ${setupOrg.id}\n`);
777
+ });
778
+ }
779
+ } catch {}
780
+
781
+ // debug-sso: test with a real connection if one exists
782
+ section('Debug SSO (workflow)');
783
+ try {
784
+ const connections = await client.sdk.sso.listConnections({ limit: 1 });
785
+ if (connections.data.length > 0) {
786
+ const connId = connections.data[0].id;
787
+ await test(`debug-sso (${connId})`, async () => {
788
+ const { runDebugSso } = await import('../src/commands/debug-sso.js');
789
+ await runDebugSso(connId, apiKey!);
790
+ });
791
+ } else {
792
+ unmuteConsole();
793
+ process.stdout.write(' (no connections found — skipping with synthetic test)\n');
794
+ muteConsole();
795
+ }
796
+ } catch {
797
+ unmuteConsole();
798
+ process.stdout.write(' (could not list connections)\n');
799
+ muteConsole();
800
+ }
801
+
802
+ // debug-sync: test with a real directory if one exists
803
+ section('Debug Sync (workflow)');
804
+ try {
805
+ const directories = await client.sdk.directorySync.listDirectories({ limit: 1 });
806
+ if (directories.data.length > 0) {
807
+ const dirId = directories.data[0].id;
808
+ await test(`debug-sync (${dirId})`, async () => {
809
+ const { runDebugSync } = await import('../src/commands/debug-sync.js');
810
+ await runDebugSync(dirId, apiKey!);
811
+ });
812
+ } else {
813
+ unmuteConsole();
814
+ process.stdout.write(' (no directories found — skipping)\n');
815
+ muteConsole();
816
+ }
817
+ } catch {
818
+ unmuteConsole();
819
+ process.stdout.write(' (could not list directories)\n');
820
+ muteConsole();
821
+ }
822
+
823
+ // onboard-user: send a test invitation (will be revoked after)
824
+ section('Onboard User (workflow)');
825
+ if (testOrgId) {
826
+ let invitationId: string | undefined;
827
+ await test('onboard-user (send invitation)', async () => {
828
+ const { runOnboardUser } = await import('../src/commands/onboard-user.js');
829
+ await runOnboardUser({ email: `smoke-test-${Date.now()}@example.com`, org: testOrgId! }, apiKey!);
830
+ const output = parseOutput() as { invitationId?: string } | null;
831
+ invitationId = output?.invitationId;
832
+ });
833
+ // Clean up: revoke the invitation
834
+ if (invitationId) {
835
+ cleanups.push(async () => {
836
+ try {
837
+ await client.sdk.userManagement.revokeInvitation(invitationId!);
838
+ process.stdout.write(` Revoked invitation: ${invitationId}\n`);
839
+ } catch {}
840
+ });
841
+ }
842
+ }
843
+
844
+ // --- Cleanup ---
845
+ unmuteConsole();
846
+ process.stdout.write('\nCleanup:\n');
847
+ for (const cleanup of cleanups.reverse()) {
848
+ try {
849
+ await cleanup();
850
+ } catch (e) {
851
+ process.stdout.write(` ⚠ Cleanup failed: ${e instanceof Error ? e.message : e}\n`);
852
+ }
853
+ }
854
+
855
+ // --- Summary ---
856
+ const passed = results.filter((r) => r.status === 'pass').length;
857
+ const failed = results.filter((r) => r.status === 'fail').length;
858
+ const skipped = results.filter((r) => r.status === 'skip').length;
859
+
860
+ process.stdout.write(`\n${'─'.repeat(40)}\n`);
861
+ process.stdout.write(`Results: ${passed} passed, ${failed} failed, ${skipped} skipped\n`);
862
+
863
+ if (failed > 0) {
864
+ process.stdout.write('\nFailures:\n');
865
+ for (const r of results.filter((r) => r.status === 'fail')) {
866
+ process.stdout.write(` ✗ ${r.name}: ${r.error}\n`);
867
+ }
868
+ realExit.call(process, 1);
869
+ }
870
+
871
+ process.stdout.write('\n');
872
+ }
873
+
874
+ run().catch((error) => {
875
+ unmuteConsole();
876
+ process.stdout.write(`\n💥 Smoke test crashed: ${error instanceof Error ? error.message : error}\n`);
877
+ if (error instanceof Error && error.stack) {
878
+ process.stdout.write(error.stack + '\n');
879
+ }
880
+ realExit.call(process, 1);
881
+ });