Skip to content

Commit 55371a9

Browse files
authored
feat: full WorkOS API emulator (emulate, dev, RBAC, webhooks, events, 84% API coverage) (#100)
* feat: add `workos emulate` command and programmatic emulator API Port the WorkOS emulator from vercel-labs/emulate into the CLI as a `workos emulate` command with a `workos/emulate` programmatic API. Core infrastructure (store, ID generation, cursor pagination, JWT, Hono server factory, auth/error middleware) lives in src/emulate/core/. WorkOS-specific plugin (12 route files covering organizations, users, memberships, connections, SSO, auth flows) lives in src/emulate/workos/. The emulator supports: - CLI: `workos emulate --port 4100 --seed config.yaml --json` - Programmatic: `createEmulator({ port: 0, seed: { users: [...] } })` - Health check at /health - Seed config via YAML/JSON (auto-detected or explicit path) - Store reset for test isolation - Random port assignment (port: 0) for parallel test suites New dependencies: hono, @hono/node-server, jose * feat: add workos dev command for local development with emulator * feat: add pipes emulation routes for user/provider connections * feat: add gen:routes codegen script for emulator route generation Developer tool that reads a WorkOS OpenAPI spec (YAML/JSON) and generates TypeScript entity types, store registrations, formatter helpers, and route stubs matching the hand-written emulate patterns. Includes 46 unit tests covering parsing, code generation, and idempotency. * chore: formatting: * docs: add local development section to README Document workos emulate, workos dev, seed configuration, programmatic API, and emulated endpoint coverage. * fix: set decomposed SDK env vars in workos dev authkit-nextjs (and the @workos-inc/node SDK) read WORKOS_API_HOSTNAME, WORKOS_API_PORT, and WORKOS_API_HTTPS — not WORKOS_API_BASE_URL. buildDevEnv now sets both styles so the emulator works with authkit SDKs and direct consumers. * feat: seed default test user in workos dev workos dev now seeds a default user (test@example.com / password) and organization so the AuthKit login flow works out of the box. The banner prints the credentials for easy copy-paste. Skipped when the user provides --seed or a workos-emulate.config.* file is auto-detected in the project directory. * chore: formatting: * fix: address emulator review findings (API key, JWT issuer, cleanup, redirects) - Expose apiKey on Emulator interface; use it in dev/emulate CLI output and buildDevEnv() so custom seed apiKeys propagate correctly - Update JWT issuer to actual bound URL after port resolution, fixing iss mismatch when using port: 0 - Clean up password resets, email verifications, and magic auths on user deletion; harden password_reset/confirm to return 404 if user was deleted - Restrict redirect_uri in authorize endpoints to localhost origins, preventing open-redirect abuse - Remove unused jose dependency * feat: emulator auth completeness + shared entity infrastructure Add token refresh with rotation, logout redirects, multi-user authorize (login_hint), impersonation, sealed sessions (AES-256-GCM), MFA challenge/verify, device authorization, and AuthKit OAuth complete flow. New grant types: refresh_token, mfa-totp, organization-selection, device_code. Includes shared entity/store/helper infrastructure for all 4 API parity phases. * feat: emulator user management — invitations, config, widgets Add invitations CRUD (create/accept/revoke/resend/by-token), config endpoints (redirect URIs, CORS origins, JWT template), user features (authorized apps, connected accounts, data providers), and widget token generation. * feat: emulator authorization — RBAC roles, permissions, FGA resources Add environment roles, org roles with priority ordering, permissions CRUD, role-permission management, authorization resources, permission checks, and role assignments. JWT tokens now include role and permissions claims for org-scoped sessions. * feat: emulator CRUD domains — directory sync, audit logs, feature flags, and more Add 9 CRUD domains: Directory Sync, Audit Logs, Feature Flags, Connect, Data Integrations, Radar, API Keys, Portal, and Legacy MFA. Each domain follows the entity/store/helper/route pattern with full test coverage. * feat: emulator events & webhooks — event bus, collection hooks, webhook delivery Add cross-cutting event system with webhook delivery. Collection-level hooks (Option A) auto-emit events on insert/update/delete for all major entities. Hybrid route-level emission for action-specific events (invitation accept/revoke, authentication succeeded). Webhook endpoints CRUD with HMAC-SHA256 signed delivery (fire-and-forget, 5s timeout). Events API with paginated listing and type filtering. Review: 2 cycles (6 findings in cycle 1, all resolved in cycle 2). * chore: formatting: * fix: resolve lint warnings in emulator spec files Remove unused variables and imports flagged by oxlint: - sessions.spec.ts: remove unused `req` helper and `headers` constant - auth.spec.ts: remove unused `user` variable in impersonator test - data-integrations.spec.ts: remove unused `getWorkOSStore` import and `store` variable - event-bus.spec.ts: use non-null assertion instead of optional chaining * feat: add emulator API coverage checker script Compares the WorkOS OpenAPI spec against emulator route registrations to report missing endpoints, extra endpoints, and coverage percentage. Usage: pnpm check:coverage ~/Developer/workos/packages/api/open-api-spec.yaml * fix: allow organization_id in refresh_token grant for switchToOrganization The refresh token handler now reads organization_id from the request body, falling back to the stored token's org. This enables the SDK's switchToOrganization flow during token refresh. * fix: alias /x/authkit/users/authenticate for SDK refresh token flow The AuthKit SDK sends refresh_token grants to /x/authkit/users/authenticate rather than /user_management/authenticate. Register the same handler on both paths so the SDK's refreshTokens() and switchToOrganization() work against the emulator. * chore: update README * docs: update README emulated endpoints table for phases 1-5 Add all new endpoint groups from auth completeness, user management, authorization, CRUD domains, and events/webhooks phases. Add seed config examples for roles, permissions, and webhook endpoints.
1 parent 1a05561 commit 55371a9

File tree

101 files changed

+13431
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+13431
-9
lines changed

README.md

Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Resource Management:
7777
api-key Manage per-org API keys
7878
org-domain Manage organization domains
7979

80+
Local Development:
81+
emulate Start a local WorkOS API emulator
82+
dev Start emulator + your app in one command
83+
8084
Workflows:
8185
seed Declarative resource provisioning from YAML
8286
setup-org One-shot organization onboarding
@@ -192,6 +196,158 @@ Inspects a directory's sync state, user/group counts, recent events, and detects
192196
workos debug-sync directory_01ABC123
193197
```
194198

199+
### Local Development
200+
201+
Test your WorkOS integration locally without hitting the live API. The emulator provides a full in-memory WorkOS API replacement with all major endpoints.
202+
203+
#### `workos dev` — One command to start everything
204+
205+
The fastest way to develop locally. Starts the emulator and your app together, auto-detecting your framework and injecting the right environment variables.
206+
207+
```bash
208+
# Auto-detects framework (Next.js, Vite, Remix, SvelteKit, etc.) and dev command
209+
workos dev
210+
211+
# Override the dev command
212+
workos dev -- npx vite --port 5173
213+
214+
# Custom emulator port and seed data
215+
workos dev --port 8080 --seed workos-emulate.config.yaml
216+
```
217+
218+
Your app receives these environment variables automatically:
219+
220+
- `WORKOS_API_BASE_URL` — points to the local emulator (e.g. `http://localhost:4100`)
221+
- `WORKOS_API_KEY` — `sk_test_default`
222+
- `WORKOS_CLIENT_ID` — `client_emulate`
223+
224+
#### `workos emulate` — Standalone emulator
225+
226+
Run the emulator on its own for CI, test suites, or when you want manual control.
227+
228+
```bash
229+
# Start with defaults (port 4100)
230+
workos emulate
231+
232+
# CI-friendly: JSON output, custom port
233+
workos emulate --port 9100 --json
234+
# → {"url":"http://localhost:9100","port":9100,"apiKey":"sk_test_default","health":"http://localhost:9100/health"}
235+
236+
# Pre-load seed data
237+
workos emulate --seed workos-emulate.config.yaml
238+
```
239+
240+
The emulator supports `GET /health` for readiness polling and shuts down cleanly on Ctrl+C.
241+
242+
#### Seed configuration
243+
244+
Create a `workos-emulate.config.yaml` (auto-detected) or pass `--seed <path>`:
245+
246+
```yaml
247+
users:
248+
- email: alice@acme.com
249+
first_name: Alice
250+
password: test123
251+
email_verified: true
252+
253+
organizations:
254+
- name: Acme Corp
255+
domains:
256+
- domain: acme.com
257+
state: verified
258+
memberships:
259+
- user_id: <user_id>
260+
role: admin
261+
262+
connections:
263+
- name: Acme SSO
264+
organization: Acme Corp
265+
connection_type: GenericSAML
266+
domains: [acme.com]
267+
268+
roles:
269+
- slug: admin
270+
name: Admin
271+
permissions: [posts:read, posts:write]
272+
273+
permissions:
274+
- slug: posts:read
275+
name: Read Posts
276+
- slug: posts:write
277+
name: Write Posts
278+
279+
webhookEndpoints:
280+
- url: http://localhost:3000/webhooks
281+
events: [user.created, organization.updated]
282+
```
283+
284+
#### Programmatic API
285+
286+
Use the emulator directly in test suites without the CLI:
287+
288+
```typescript
289+
import { createEmulator } from 'workos/emulate';
290+
291+
const emulator = await createEmulator({
292+
port: 0, // random available port
293+
seed: {
294+
users: [{ email: 'test@example.com', password: 'secret' }],
295+
},
296+
});
297+
298+
// Use emulator.url as your WORKOS_API_BASE_URL
299+
const res = await fetch(`${emulator.url}/user_management/users`, {
300+
headers: { Authorization: 'Bearer sk_test_default' },
301+
});
302+
303+
// Reset between tests (clears data, re-applies seed)
304+
emulator.reset();
305+
306+
// Clean up
307+
await emulator.close();
308+
```
309+
310+
#### Emulated endpoints
311+
312+
The emulator covers the full WorkOS API surface (~84% of OpenAPI spec endpoints). Run `pnpm check:coverage <openapi-spec>` to see exact coverage.
313+
314+
| Endpoint Group | Routes |
315+
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
316+
| Organizations | CRUD, external_id lookup, domain management |
317+
| Users | CRUD, email uniqueness, password management |
318+
| Organization memberships | CRUD, role assignment, deactivate/reactivate |
319+
| Organization domains | CRUD, verification |
320+
| SSO connections | CRUD, domain-based lookup |
321+
| SSO flow | Authorize, token exchange, profile, JWKS, SSO logout |
322+
| AuthKit | OAuth authorize (login_hint, multi-user), authenticate (7 grant types incl. refresh_token, MFA TOTP, org selection, device code), PKCE, sealed sessions, impersonation |
323+
| Sessions | List, revoke, logout redirect, JWKS per client |
324+
| Email verification | Send code, confirm |
325+
| Password reset | Create token, confirm |
326+
| Magic auth | Create code |
327+
| Auth factors | TOTP enrollment, delete |
328+
| MFA challenges | Create challenge, verify code |
329+
| Invitations | CRUD, accept, revoke, resend, get by token |
330+
| Config | Redirect URIs, CORS origins, JWT template |
331+
| User features | Authorized apps, connected accounts, data providers |
332+
| Widgets | Token generation |
333+
| Authorization (RBAC) | Environment roles, org roles (priority ordering), permissions, role-permission management |
334+
| Authorization (FGA) | Resources CRUD, permission checks, role assignments |
335+
| Directory Sync | List/get/delete directories, users, groups |
336+
| Audit Logs | Actions, schemas, events, exports, org config/retention |
337+
| Feature Flags | List/get, enable/disable, targets, org/user evaluations |
338+
| Connect | Applications CRUD, client secrets |
339+
| Data Integrations | OAuth authorize + token exchange |
340+
| Radar | Attempts list/get, allow/deny lists |
341+
| API Keys | Validate, delete, list by org |
342+
| Portal | Generate admin portal links |
343+
| Legacy MFA | Enroll/get/delete factors, challenge/verify |
344+
| Webhook Endpoints | CRUD with auto-generated secrets, secret masking |
345+
| Events | Paginated event stream with type filtering |
346+
| Event Bus | Auto-emits events on entity CRUD via collection hooks, fire-and-forget webhook delivery with HMAC signatures |
347+
| Pipes | Connection CRUD, mock `getAccessToken()` |
348+
349+
JWT tokens include `role` and `permissions` claims for org-scoped sessions. All list endpoints support cursor pagination (`before`, `after`, `limit`, `order`). Error responses match the WorkOS format (`{ message, code, errors }`).
350+
195351
### Environment Management
196352

197353
```bash
@@ -466,12 +622,13 @@ workos install --api-key sk_test_xxx --client-id client_xxx --no-commit 2>/dev/n
466622
467623
### Environment Variables
468624
469-
| Variable | Effect |
470-
| ------------------------ | -------------------------------------------------------- |
471-
| `WORKOS_API_KEY` | API key for management commands (bypasses stored config) |
472-
| `WORKOS_NO_PROMPT=1` | Force non-interactive mode + JSON output |
473-
| `WORKOS_FORCE_TTY=1` | Force interactive mode even when piped |
474-
| `WORKOS_TELEMETRY=false` | Disable telemetry |
625+
| Variable | Effect |
626+
| ------------------------ | --------------------------------------------------------- |
627+
| `WORKOS_API_KEY` | API key for management commands (bypasses stored config) |
628+
| `WORKOS_API_BASE_URL` | Override API base URL (set automatically by `workos dev`) |
629+
| `WORKOS_NO_PROMPT=1` | Force non-interactive mode + JSON output |
630+
| `WORKOS_FORCE_TTY=1` | Force interactive mode even when piped |
631+
| `WORKOS_TELEMETRY=false` | Disable telemetry |
475632
476633
### Command Discovery
477634

package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
3434
"typescript": {
3535
"definition": "dist/index.d.ts"
3636
},
37+
"exports": {
38+
".": {
39+
"import": "./dist/index.js",
40+
"types": "./dist/index.d.ts"
41+
},
42+
"./emulate": {
43+
"import": "./dist/emulate/index.js",
44+
"types": "./dist/emulate/index.d.ts"
45+
}
46+
},
3747
"dependencies": {
3848
"@anthropic-ai/claude-agent-sdk": "~0.2.62",
3949
"@anthropic-ai/sdk": "^0.78.0",
@@ -51,6 +61,8 @@
5161
"semver": "^7.7.4",
5262
"uuid": "^13.0.0",
5363
"xstate": "^5.28.0",
64+
"hono": "^4",
65+
"@hono/node-server": "^1",
5466
"yaml": "^2.8.2",
5567
"yargs": "^18.0.0",
5668
"zod": "^4.3.6"
@@ -96,7 +108,9 @@
96108
"eval:diff": "tsx tests/evals/index.ts diff",
97109
"eval:prune": "tsx tests/evals/index.ts prune",
98110
"eval:logs": "tsx tests/evals/index.ts logs",
99-
"eval:show": "tsx tests/evals/index.ts show"
111+
"eval:show": "tsx tests/evals/index.ts show",
112+
"gen:routes": "tsx scripts/gen-routes.ts",
113+
"check:coverage": "tsx scripts/check-coverage.ts"
100114
},
101115
"author": "WorkOS",
102116
"license": "MIT"

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)