From a9160abc0faaa79b0a69c32012ca780b24927840 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Wed, 25 Mar 2026 19:51:57 -0500 Subject: [PATCH 1/4] refactor: emulator simplification from code review - Cache getWorkOSStore() on Store instance (avoids 37 map lookups per call) - Use indexed findOneBy() for auth code + SSO code lookup (O(1) vs O(n)) - Hoist authMiddleware to single instance (was creating ~25 closures) - Add Collection.deleteBy() for cascade delete patterns - Consistent getWorkOSStore placement (once at setup, not per-handler) - Remove dead seedDefaults no-op and phase marker comments --- src/emulate/core/server.ts | 86 ++++++++++--------- src/emulate/core/store.ts | 6 ++ src/emulate/workos/entities.ts | 2 - src/emulate/workos/index.ts | 8 +- src/emulate/workos/routes/auth.ts | 2 +- .../workos/routes/authorization-checks.ts | 6 +- .../workos/routes/authorization-org-roles.ts | 10 +-- .../routes/authorization-permissions.ts | 6 +- .../workos/routes/authorization-resources.ts | 9 +- .../workos/routes/authorization-roles.ts | 8 +- src/emulate/workos/routes/sso.ts | 2 +- src/emulate/workos/store.ts | 10 ++- 12 files changed, 68 insertions(+), 87 deletions(-) diff --git a/src/emulate/core/server.ts b/src/emulate/core/server.ts index 285b160..6b60eb7 100644 --- a/src/emulate/core/server.ts +++ b/src/emulate/core/server.ts @@ -33,57 +33,59 @@ export function createServer(plugin: ServicePlugin, options: ServerOptions = {}) return c.json(jwt.getJWKS()); }); - // Auth middleware for API routes - app.use('/api/*', authMiddleware(apiKeys)); + // Auth middleware — single instance, shared across all routes + const auth = authMiddleware(apiKeys); + + const PUBLIC_PATHS = new Set([ + '/user_management/authorize', + '/user_management/authenticate', + '/user_management/sessions/logout', + ]); + + app.use('/api/*', auth); app.use('/user_management/*', async (c, next) => { const path = new URL(c.req.url).pathname; - // Public endpoints (no auth required) - if ( - path === '/user_management/authorize' || - path === '/user_management/authenticate' || - path === '/user_management/sessions/logout' || - path.startsWith('/user_management/sessions/jwks/') - ) { + if (PUBLIC_PATHS.has(path) || path.startsWith('/user_management/sessions/jwks/')) { return next(); } - return authMiddleware(apiKeys)(c, next); + return auth(c, next); }); - app.use('/x/authkit/*', authMiddleware(apiKeys)); - app.use('/organizations', authMiddleware(apiKeys)); - app.use('/organizations/*', authMiddleware(apiKeys)); - app.use('/organization_memberships', authMiddleware(apiKeys)); - app.use('/organization_memberships/*', authMiddleware(apiKeys)); - app.use('/organization_domains', authMiddleware(apiKeys)); - app.use('/organization_domains/*', authMiddleware(apiKeys)); - app.use('/connections', authMiddleware(apiKeys)); - app.use('/connections/*', authMiddleware(apiKeys)); - app.use('/directories', authMiddleware(apiKeys)); - app.use('/directories/*', authMiddleware(apiKeys)); - app.use('/directory_groups', authMiddleware(apiKeys)); - app.use('/directory_groups/*', authMiddleware(apiKeys)); - app.use('/directory_users', authMiddleware(apiKeys)); - app.use('/directory_users/*', authMiddleware(apiKeys)); - app.use('/events', authMiddleware(apiKeys)); - app.use('/events/*', authMiddleware(apiKeys)); - app.use('/pipes/*', authMiddleware(apiKeys)); - app.use('/audit_logs/*', authMiddleware(apiKeys)); - app.use('/feature-flags', authMiddleware(apiKeys)); - app.use('/feature-flags/*', authMiddleware(apiKeys)); - app.use('/connect/*', authMiddleware(apiKeys)); + app.use('/x/authkit/*', auth); + app.use('/organizations', auth); + app.use('/organizations/*', auth); + app.use('/organization_memberships', auth); + app.use('/organization_memberships/*', auth); + app.use('/organization_domains', auth); + app.use('/organization_domains/*', auth); + app.use('/connections', auth); + app.use('/connections/*', auth); + app.use('/directories', auth); + app.use('/directories/*', auth); + app.use('/directory_groups', auth); + app.use('/directory_groups/*', auth); + app.use('/directory_users', auth); + app.use('/directory_users/*', auth); + app.use('/events', auth); + app.use('/events/*', auth); + app.use('/pipes/*', auth); + app.use('/audit_logs/*', auth); + app.use('/feature-flags', auth); + app.use('/feature-flags/*', auth); + app.use('/connect/*', auth); app.use('/data-integrations/*', async (c, next) => { const path = new URL(c.req.url).pathname; if (path.endsWith('/authorize')) return next(); - return authMiddleware(apiKeys)(c, next); + return auth(c, next); }); - app.use('/radar/*', authMiddleware(apiKeys)); - app.use('/api_keys', authMiddleware(apiKeys)); - app.use('/api_keys/*', authMiddleware(apiKeys)); - app.use('/portal/*', authMiddleware(apiKeys)); - app.use('/webhook_endpoints', authMiddleware(apiKeys)); - app.use('/webhook_endpoints/*', authMiddleware(apiKeys)); - app.use('/auth/factors', authMiddleware(apiKeys)); - app.use('/auth/factors/*', authMiddleware(apiKeys)); - app.use('/auth/challenges/*', authMiddleware(apiKeys)); + app.use('/radar/*', auth); + app.use('/api_keys', auth); + app.use('/api_keys/*', auth); + app.use('/portal/*', auth); + app.use('/webhook_endpoints', auth); + app.use('/webhook_endpoints/*', auth); + app.use('/auth/factors', auth); + app.use('/auth/factors/*', auth); + app.use('/auth/challenges/*', auth); // Rate limiting const rateLimitCounters = new Map(); diff --git a/src/emulate/core/store.ts b/src/emulate/core/store.ts index 6728cdf..9ccb5a3 100644 --- a/src/emulate/core/store.ts +++ b/src/emulate/core/store.ts @@ -113,6 +113,12 @@ export class Collection { return this.items.delete(id); } + deleteBy(field: keyof T, value: string | number): number { + const items = this.findBy(field, value); + for (const item of items) this.delete(item.id); + return items.length; + } + setHooks(hooks: CollectionHooks): void { this.hooks = hooks; } diff --git a/src/emulate/workos/entities.ts b/src/emulate/workos/entities.ts index 4aeb07e..5f9bf31 100644 --- a/src/emulate/workos/entities.ts +++ b/src/emulate/workos/entities.ts @@ -267,8 +267,6 @@ export interface WorkOSRoleAssignment extends Entity { role_id: string; } -// --- Phase 4: CRUD Domains --- - export interface WorkOSDirectory extends Entity { object: 'directory'; name: string; diff --git a/src/emulate/workos/index.ts b/src/emulate/workos/index.ts index 1f3f674..5f2e827 100644 --- a/src/emulate/workos/index.ts +++ b/src/emulate/workos/index.ts @@ -146,10 +146,6 @@ export interface WorkOSSeedConfig { webhookEndpoints?: WorkOSSeedWebhookEndpoint[]; } -function seedDefaults(_store: Store, _baseUrl: string): void { - // No default seed data — users provide their own via config -} - export function seedFromConfig(store: Store, _baseUrl: string, config: WorkOSSeedConfig): void { const ws = getWorkOSStore(store); @@ -436,8 +432,8 @@ export const workosPlugin: ServicePlugin = { onDelete: (g) => eventBus.emit({ event: 'directory_group.deleted', data: formatDirectoryGroup(g) }), }); }, - seed(store: Store, baseUrl: string): void { - seedDefaults(store, baseUrl); + seed(_store: Store, _baseUrl: string): void { + // No default seed data — users provide their own via seedFromConfig }, }; diff --git a/src/emulate/workos/routes/auth.ts b/src/emulate/workos/routes/auth.ts index 11317d1..3e8e925 100644 --- a/src/emulate/workos/routes/auth.ts +++ b/src/emulate/workos/routes/auth.ts @@ -116,7 +116,7 @@ export function authRoutes(ctx: RouteContext): void { const code = body.code as string; if (!code) throw new WorkOSApiError(400, 'code is required', 'invalid_request'); - const authCode = ws.authCodes.all().find((ac) => ac.code === code); + const authCode = ws.authCodes.findOneBy('code', code); if (!authCode) throw new WorkOSApiError(400, 'Invalid code', 'invalid_code'); if (isExpired(authCode.expires_at)) { throw new WorkOSApiError(400, 'Code has expired', 'expired_code'); diff --git a/src/emulate/workos/routes/authorization-checks.ts b/src/emulate/workos/routes/authorization-checks.ts index a53bf88..4368e5e 100644 --- a/src/emulate/workos/routes/authorization-checks.ts +++ b/src/emulate/workos/routes/authorization-checks.ts @@ -42,10 +42,10 @@ function getPermissionsForMembership(ws: ReturnType, memb export function authorizationCheckRoutes(ctx: RouteContext): void { const { app, store } = ctx; + const ws = getWorkOSStore(store); // Permission check app.post('/authorization/organization_memberships/:id/check', async (c) => { - const ws = getWorkOSStore(store); const membershipId = c.req.param('id'); const membership = ws.organizationMemberships.get(membershipId); if (!membership) throw notFound('OrganizationMembership'); @@ -62,7 +62,6 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { // List resources accessible to a membership (all resources in the membership's org) app.get('/authorization/organization_memberships/:id/resources', (c) => { - const ws = getWorkOSStore(store); const membershipId = c.req.param('id'); const membership = ws.organizationMemberships.get(membershipId); if (!membership) throw notFound('OrganizationMembership'); @@ -84,7 +83,6 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { // List role assignments for a membership app.get('/authorization/organization_memberships/:id/role_assignments', (c) => { - const ws = getWorkOSStore(store); const membershipId = c.req.param('id'); const membership = ws.organizationMemberships.get(membershipId); if (!membership) throw notFound('OrganizationMembership'); @@ -106,7 +104,6 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { // Create role assignment app.post('/authorization/organization_memberships/:id/role_assignments', async (c) => { - const ws = getWorkOSStore(store); const membershipId = c.req.param('id'); const membership = ws.organizationMemberships.get(membershipId); if (!membership) throw notFound('OrganizationMembership'); @@ -131,7 +128,6 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { // Delete role assignment app.delete('/authorization/organization_memberships/:id/role_assignments/:assignmentId', (c) => { - const ws = getWorkOSStore(store); const membershipId = c.req.param('id'); const assignmentId = c.req.param('assignmentId'); diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index e3cd4f8..8788d66 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -4,9 +4,9 @@ import { formatRole, formatPermission, parseListParams } from '../helpers.js'; export function authorizationOrgRoleRoutes(ctx: RouteContext): void { const { app, store } = ctx; + const ws = getWorkOSStore(store); app.post('/authorization/organizations/:orgId/roles', async (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const org = ws.organizations.get(orgId); if (!org) throw notFound('Organization'); @@ -47,7 +47,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.get('/authorization/organizations/:orgId/roles', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const url = new URL(c.req.url); const params = parseListParams(url); @@ -66,7 +65,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { // Priority ordering — must be registered before :slug routes app.put('/authorization/organizations/:orgId/roles/priority', async (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const body = await parseJsonBody(c); const slugs = body.slugs as string[]; @@ -96,7 +94,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.get('/authorization/organizations/:orgId/roles/:slug', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const role = ws.roles @@ -107,7 +104,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.put('/authorization/organizations/:orgId/roles/:slug', async (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const role = ws.roles @@ -127,7 +123,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.delete('/authorization/organizations/:orgId/roles/:slug', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const role = ws.roles @@ -147,7 +142,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { // Org role permissions app.get('/authorization/organizations/:orgId/roles/:slug/permissions', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const role = ws.roles @@ -166,7 +160,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.post('/authorization/organizations/:orgId/roles/:slug/permissions', async (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const role = ws.roles @@ -201,7 +194,6 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); app.delete('/authorization/organizations/:orgId/roles/:slug/permissions/:permissionSlug', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const slug = c.req.param('slug'); const permissionSlug = c.req.param('permissionSlug'); diff --git a/src/emulate/workos/routes/authorization-permissions.ts b/src/emulate/workos/routes/authorization-permissions.ts index 5275d14..d8e5954 100644 --- a/src/emulate/workos/routes/authorization-permissions.ts +++ b/src/emulate/workos/routes/authorization-permissions.ts @@ -4,9 +4,9 @@ import { formatPermission, parseListParams } from '../helpers.js'; export function authorizationPermissionRoutes(ctx: RouteContext): void { const { app, store } = ctx; + const ws = getWorkOSStore(store); app.post('/authorization/permissions', async (c) => { - const ws = getWorkOSStore(store); const body = await parseJsonBody(c); const slug = body.slug as string; const name = body.name as string; @@ -34,7 +34,6 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { }); app.get('/authorization/permissions', (c) => { - const ws = getWorkOSStore(store); const url = new URL(c.req.url); const params = parseListParams(url); @@ -47,7 +46,6 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { }); app.get('/authorization/permissions/:slug', (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const permission = ws.permissions.findOneBy('slug', slug); if (!permission) throw notFound('Permission'); @@ -55,7 +53,6 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { }); app.put('/authorization/permissions/:slug', async (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const permission = ws.permissions.findOneBy('slug', slug); if (!permission) throw notFound('Permission'); @@ -70,7 +67,6 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { }); app.delete('/authorization/permissions/:slug', (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const permission = ws.permissions.findOneBy('slug', slug); if (!permission) throw notFound('Permission'); diff --git a/src/emulate/workos/routes/authorization-resources.ts b/src/emulate/workos/routes/authorization-resources.ts index e9b9a1b..cbe270f 100644 --- a/src/emulate/workos/routes/authorization-resources.ts +++ b/src/emulate/workos/routes/authorization-resources.ts @@ -4,9 +4,9 @@ import { formatAuthorizationResource, formatMembership, parseListParams } from ' export function authorizationResourceRoutes(ctx: RouteContext): void { const { app, store } = ctx; + const ws = getWorkOSStore(store); app.post('/authorization/resources', async (c) => { - const ws = getWorkOSStore(store); const body = await parseJsonBody(c); const resourceTypeSlug = body.resource_type_slug as string; @@ -35,7 +35,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { }); app.get('/authorization/resources', (c) => { - const ws = getWorkOSStore(store); const url = new URL(c.req.url); const params = parseListParams(url); const organizationId = url.searchParams.get('organization_id') ?? undefined; @@ -58,7 +57,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { }); app.get('/authorization/resources/:resource_id', (c) => { - const ws = getWorkOSStore(store); const resourceId = c.req.param('resource_id'); const resource = ws.authorizationResources.get(resourceId); if (!resource) throw notFound('AuthorizationResource'); @@ -66,7 +64,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { }); app.put('/authorization/resources/:resource_id', async (c) => { - const ws = getWorkOSStore(store); const resourceId = c.req.param('resource_id'); const resource = ws.authorizationResources.get(resourceId); if (!resource) throw notFound('AuthorizationResource'); @@ -80,7 +77,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { }); app.delete('/authorization/resources/:resource_id', (c) => { - const ws = getWorkOSStore(store); const resourceId = c.req.param('resource_id'); const resource = ws.authorizationResources.get(resourceId); if (!resource) throw notFound('AuthorizationResource'); @@ -91,7 +87,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { // Memberships with access to a resource (by resource ID) app.get('/authorization/resources/:resource_id/organization_memberships', (c) => { - const ws = getWorkOSStore(store); const resourceId = c.req.param('resource_id'); const resource = ws.authorizationResources.get(resourceId); if (!resource) throw notFound('AuthorizationResource'); @@ -106,7 +101,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { // Get resource by type + external ID within an org app.get('/authorization/organizations/:orgId/resources/:type_slug/:external_id', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const typeSlug = c.req.param('type_slug'); const externalId = c.req.param('external_id'); @@ -120,7 +114,6 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { // Memberships for resource by type + external ID within an org app.get('/authorization/organizations/:orgId/resources/:type_slug/:external_id/organization_memberships', (c) => { - const ws = getWorkOSStore(store); const orgId = c.req.param('orgId'); const typeSlug = c.req.param('type_slug'); const externalId = c.req.param('external_id'); diff --git a/src/emulate/workos/routes/authorization-roles.ts b/src/emulate/workos/routes/authorization-roles.ts index fc7c5a8..6bdb9ef 100644 --- a/src/emulate/workos/routes/authorization-roles.ts +++ b/src/emulate/workos/routes/authorization-roles.ts @@ -4,9 +4,9 @@ import { formatRole, formatPermission, parseListParams } from '../helpers.js'; export function authorizationRoleRoutes(ctx: RouteContext): void { const { app, store } = ctx; + const ws = getWorkOSStore(store); app.post('/authorization/roles', async (c) => { - const ws = getWorkOSStore(store); const body = await parseJsonBody(c); const slug = body.slug as string; const name = body.name as string; @@ -39,7 +39,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { }); app.get('/authorization/roles', (c) => { - const ws = getWorkOSStore(store); const url = new URL(c.req.url); const params = parseListParams(url); @@ -56,7 +55,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { }); app.get('/authorization/roles/:slug', (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); if (!role) throw notFound('Role'); @@ -64,7 +62,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { }); app.put('/authorization/roles/:slug', async (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); if (!role) throw notFound('Role'); @@ -81,7 +78,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { }); app.delete('/authorization/roles/:slug', (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); if (!role) throw notFound('Role'); @@ -98,7 +94,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { // Role permissions management app.get('/authorization/roles/:slug/permissions', (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); if (!role) throw notFound('Role'); @@ -114,7 +109,6 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { }); app.post('/authorization/roles/:slug/permissions', async (c) => { - const ws = getWorkOSStore(store); const slug = c.req.param('slug'); const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); if (!role) throw notFound('Role'); diff --git a/src/emulate/workos/routes/sso.ts b/src/emulate/workos/routes/sso.ts index 75a586c..dd49b36 100644 --- a/src/emulate/workos/routes/sso.ts +++ b/src/emulate/workos/routes/sso.ts @@ -82,7 +82,7 @@ export function ssoRoutes(ctx: RouteContext): void { throw new WorkOSApiError(400, 'code is required', 'invalid_request'); } - const auth = ws.ssoAuthorizations.all().find((a) => a.code === code); + const auth = ws.ssoAuthorizations.findOneBy('code', code); if (!auth) { throw new WorkOSApiError(400, 'Invalid authorization code', 'invalid_code'); } diff --git a/src/emulate/workos/store.ts b/src/emulate/workos/store.ts index 12de6ee..565d116 100644 --- a/src/emulate/workos/store.ts +++ b/src/emulate/workos/store.ts @@ -91,8 +91,13 @@ export interface WorkOSStore { webhookEndpoints: Collection; } +const CACHE_KEY = '_workos_store'; + export function getWorkOSStore(store: Store): WorkOSStore { - return { + const cached = store.getData(CACHE_KEY); + if (cached) return cached; + + const ws: WorkOSStore = { organizations: store.collection('workos.organizations', 'org', ['name', 'external_id']), organizationDomains: store.collection('workos.organization_domains', 'org_domain', [ 'organization_id', @@ -190,4 +195,7 @@ export function getWorkOSStore(store: Store): WorkOSStore { events: store.collection('workos.events', 'evt', ['event']), webhookEndpoints: store.collection('workos.webhook_endpoints', 'we', ['url']), }; + + store.setData(CACHE_KEY, ws); + return ws; } From 4c765ae7a26fc599edc5c549ef8c59c94a54b8a2 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 26 Mar 2026 14:55:53 -0500 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20emulator=20simplification=20pha?= =?UTF-8?q?se=201=20=E2=80=94=20shared=20infrastructure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create constants.ts with typed STORE_KEYS, STORE_KEY_PREFIXES, and EVENTS - Unify ID_PREFIXES in core/id.ts with workos/store.ts (add 15 missing entries, fix event prefix) - Optimize Collection.count() to iterate in-place instead of materializing full array - Move parseListParams to core/pagination.ts, remove redundant limit clamping - Add generic formatEntity() and formatListResponse() helpers - Replace 34 trivial formatters with formatEntity() calls (5 with custom exclude sets) - Update 22 list response sites to use formatListResponse() - Replace all magic strings with typed constants across 31 files Review: 1 cycle, 4 findings (0 critical, 0 high, 2 medium, 2 low — all addressed) --- src/emulate/core/id.ts | 17 +- src/emulate/core/index.ts | 2 +- src/emulate/core/pagination.ts | 8 + src/emulate/core/store.ts | 6 +- src/emulate/workos/constants.ts | 59 +++ src/emulate/workos/event-bus.ts | 3 +- src/emulate/workos/helpers.ts | 437 +++--------------- src/emulate/workos/index.ts | 69 +-- src/emulate/workos/routes/api-keys.ts | 13 +- src/emulate/workos/routes/audit-logs.ts | 15 +- src/emulate/workos/routes/auth.ts | 11 +- .../workos/routes/authorization-checks.ts | 16 +- .../workos/routes/authorization-org-roles.ts | 10 +- .../routes/authorization-permissions.ts | 10 +- .../workos/routes/authorization-resources.ts | 10 +- .../workos/routes/authorization-roles.ts | 10 +- src/emulate/workos/routes/config.ts | 5 +- src/emulate/workos/routes/connect.ts | 15 +- src/emulate/workos/routes/connections.ts | 10 +- src/emulate/workos/routes/directories.ts | 22 +- src/emulate/workos/routes/events.ts | 10 +- src/emulate/workos/routes/feature-flags.ts | 10 +- src/emulate/workos/routes/invitations.ts | 23 +- src/emulate/workos/routes/memberships.ts | 10 +- src/emulate/workos/routes/organizations.ts | 10 +- src/emulate/workos/routes/pipes.ts | 10 +- src/emulate/workos/routes/radar.ts | 10 +- src/emulate/workos/routes/sso.ts | 11 +- src/emulate/workos/routes/users.ts | 10 +- .../workos/routes/webhook-endpoints.ts | 10 +- src/emulate/workos/store.ts | 196 +++++--- 31 files changed, 408 insertions(+), 650 deletions(-) create mode 100644 src/emulate/workos/constants.ts diff --git a/src/emulate/core/id.ts b/src/emulate/core/id.ts index 3e58e9e..bf84fc7 100644 --- a/src/emulate/core/id.ts +++ b/src/emulate/core/id.ts @@ -41,7 +41,7 @@ export const ID_PREFIXES = { directory: 'directory', directory_user: 'directory_user', directory_group: 'directory_grp', - event: 'event', + event: 'evt', invitation: 'inv', session: 'session', email_verification: 'email_verification', @@ -49,9 +49,23 @@ export const ID_PREFIXES = { magic_auth: 'magic_auth', authentication_factor: 'auth_factor', authentication_challenge: 'auth_challenge', + authorization_code: 'auth_code', + identity: 'identity', + sso_authorization: 'sso_auth', + refresh_token: 'ref', + device_authorization: 'dev_auth', api_key: 'api_key', profile: 'prof', pipe_connection: 'pipe_conn', + redirect_uri: 'redir', + cors_origin: 'cors', + authorized_application: 'auth_app', + connected_account: 'conn_acct', + role: 'role', + permission: 'perm', + role_permission: 'rp', + authorization_resource: 'auth_res', + role_assignment: 'ra', audit_log_action: 'audit_action', audit_log_event: 'audit_event', audit_log_export: 'audit_export', @@ -61,4 +75,5 @@ export const ID_PREFIXES = { client_secret: 'client_secret', data_integration_auth: 'di_auth', radar_attempt: 'radar_attempt', + webhook_endpoint: 'we', } as const; diff --git a/src/emulate/core/index.ts b/src/emulate/core/index.ts index 3e356de..8046138 100644 --- a/src/emulate/core/index.ts +++ b/src/emulate/core/index.ts @@ -8,7 +8,7 @@ export { type CollectionHooks, } from './store.js'; export { generateId, resetIdState, ID_PREFIXES } from './id.js'; -export { cursorPaginate, type CursorPaginationOptions, type CursorPaginatedResult } from './pagination.js'; +export { parseListParams, cursorPaginate, type CursorPaginationOptions, type CursorPaginatedResult } from './pagination.js'; export { JWTManager, type JWTPayload } from './jwt.js'; export { createServer, type ServerOptions } from './server.js'; export { type ServicePlugin, type RouteContext } from './plugin.js'; diff --git a/src/emulate/core/pagination.ts b/src/emulate/core/pagination.ts index 08f84b2..8010a7a 100644 --- a/src/emulate/core/pagination.ts +++ b/src/emulate/core/pagination.ts @@ -21,6 +21,14 @@ export interface CursorPaginatedResult { }; } +export function parseListParams(url: URL) { + const limit = parseInt(url.searchParams.get('limit') ?? '10') || 10; + const order = (url.searchParams.get('order') as 'asc' | 'desc') ?? 'desc'; + const before = url.searchParams.get('before') ?? undefined; + const after = url.searchParams.get('after') ?? undefined; + return { limit, order, before, after }; +} + export function cursorPaginate( items: T[], options: CursorPaginationOptions = {}, diff --git a/src/emulate/core/store.ts b/src/emulate/core/store.ts index 9ccb5a3..d0c6f5a 100644 --- a/src/emulate/core/store.ts +++ b/src/emulate/core/store.ts @@ -133,7 +133,11 @@ export class Collection { count(filter?: FilterFn): number { if (!filter) return this.items.size; - return this.all().filter(filter).length; + let n = 0; + for (const item of this.items.values()) { + if (filter(item)) n++; + } + return n; } clear(): void { diff --git a/src/emulate/workos/constants.ts b/src/emulate/workos/constants.ts new file mode 100644 index 0000000..851808f --- /dev/null +++ b/src/emulate/workos/constants.ts @@ -0,0 +1,59 @@ +/** Typed keys for Store.getData/setData */ +export const STORE_KEYS = { + workosStore: '_workos_store', + eventBus: 'eventBus', + apiKeyMap: 'apiKeyMap', + jwtTemplate: 'jwt_template', +} as const; + +/** Prefix for dynamic store keys */ +export const STORE_KEY_PREFIXES = { + pendingAuth: 'pending_auth:', + ssoToken: 'sso_token:', + ssoLogout: 'sso_logout:', + auditSchema: 'audit_schema_', + radarIpList: 'radar_ip_list', +} as const; + +/** All WorkOS webhook event names */ +export const EVENTS = { + userCreated: 'user.created', + userUpdated: 'user.updated', + userDeleted: 'user.deleted', + organizationCreated: 'organization.created', + organizationUpdated: 'organization.updated', + organizationDeleted: 'organization.deleted', + organizationDomainCreated: 'organization_domain.created', + organizationDomainVerified: 'organization_domain.verified', + organizationDomainUpdated: 'organization_domain.updated', + organizationDomainDeleted: 'organization_domain.deleted', + organizationMembershipCreated: 'organization_membership.created', + organizationMembershipUpdated: 'organization_membership.updated', + organizationMembershipDeleted: 'organization_membership.deleted', + connectionCreated: 'connection.created', + connectionUpdated: 'connection.updated', + connectionDeleted: 'connection.deleted', + sessionCreated: 'session.created', + sessionRevoked: 'session.revoked', + invitationCreated: 'invitation.created', + invitationAccepted: 'invitation.accepted', + invitationRevoked: 'invitation.revoked', + invitationResent: 'invitation.resent', + roleCreated: 'role.created', + roleUpdated: 'role.updated', + roleDeleted: 'role.deleted', + permissionCreated: 'permission.created', + permissionUpdated: 'permission.updated', + permissionDeleted: 'permission.deleted', + directoryCreated: 'directory.created', + directoryUpdated: 'directory.updated', + directoryDeleted: 'directory.deleted', + directoryUserCreated: 'directory_user.created', + directoryUserUpdated: 'directory_user.updated', + directoryUserDeleted: 'directory_user.deleted', + directoryGroupCreated: 'directory_group.created', + directoryGroupUpdated: 'directory_group.updated', + directoryGroupDeleted: 'directory_group.deleted', +} as const; + +export type WorkOSEventName = (typeof EVENTS)[keyof typeof EVENTS]; diff --git a/src/emulate/workos/event-bus.ts b/src/emulate/workos/event-bus.ts index 33b8b2a..413c3d0 100644 --- a/src/emulate/workos/event-bus.ts +++ b/src/emulate/workos/event-bus.ts @@ -2,9 +2,10 @@ import type { Store } from '../core/index.js'; import { getWorkOSStore } from './store.js'; import type { WorkOSWebhookEndpoint, WorkOSEvent } from './entities.js'; import { signWebhookPayload } from './webhook-signer.js'; +import type { WorkOSEventName } from './constants.js'; export interface EventPayload { - event: string; + event: WorkOSEventName | string; data: Record; environment_id?: string; } diff --git a/src/emulate/workos/helpers.ts b/src/emulate/workos/helpers.ts index 8040c94..3a42ebc 100644 --- a/src/emulate/workos/helpers.ts +++ b/src/emulate/workos/helpers.ts @@ -1,5 +1,5 @@ import { randomBytes, createHash, createCipheriv } from 'node:crypto'; -import { WorkOSApiError } from '../core/index.js'; +import { WorkOSApiError, type CursorPaginatedResult, type Entity } from '../core/index.js'; import type { WorkOSStore } from './store.js'; import type { WorkOSOrganization, @@ -41,6 +41,31 @@ import type { WorkOSWebhookEndpoint, } from './entities.js'; +const INTERNAL_FIELDS = new Set(['password_hash', 'code_challenge', 'code_challenge_method']); + +export function formatEntity( + entity: T, + opts?: { exclude?: Set }, +): Record { + const exclude = opts?.exclude ?? INTERNAL_FIELDS; + const result: Record = {}; + for (const [key, value] of Object.entries(entity)) { + if (!exclude.has(key)) result[key] = value; + } + return result; +} + +export function formatListResponse( + result: CursorPaginatedResult, + formatter: (item: T) => Record, +): { object: 'list'; data: Record[]; list_metadata: { before: string | null; after: string | null } } { + return { + object: 'list', + data: result.data.map(formatter), + list_metadata: result.list_metadata, + }; +} + export function formatOrganization(org: WorkOSOrganization, ws: WorkOSStore): Record { const domains = ws.organizationDomains.findBy('organization_id', org.id).map(formatDomain); @@ -58,128 +83,41 @@ export function formatOrganization(org: WorkOSOrganization, ws: WorkOSStore): Re } export function formatDomain(domain: WorkOSOrganizationDomain): Record { - return { - object: 'organization_domain', - id: domain.id, - organization_id: domain.organization_id, - domain: domain.domain, - state: domain.state, - verification_strategy: domain.verification_strategy, - verification_token: domain.verification_token, - verification_prefix: domain.verification_prefix, - created_at: domain.created_at, - updated_at: domain.updated_at, - }; + return formatEntity(domain); } export function formatMembership(m: WorkOSOrganizationMembership): Record { - return { - object: 'organization_membership', - id: m.id, - organization_id: m.organization_id, - user_id: m.user_id, - role: m.role, - status: m.status, - external_id: m.external_id, - metadata: m.metadata, - created_at: m.created_at, - updated_at: m.updated_at, - }; + return formatEntity(m); } +const USER_EXCLUDE = new Set([...INTERNAL_FIELDS, 'impersonator']); + export function formatUser(user: WorkOSUser): Record { - return { - object: 'user', - id: user.id, - email: user.email, - first_name: user.first_name, - last_name: user.last_name, - email_verified: user.email_verified, - profile_picture_url: user.profile_picture_url, - last_sign_in_at: user.last_sign_in_at, - external_id: user.external_id, - metadata: user.metadata, - locale: user.locale, - created_at: user.created_at, - updated_at: user.updated_at, - }; + return formatEntity(user, { exclude: USER_EXCLUDE }); } export function formatSession(s: WorkOSSession): Record { - return { - object: 'session', - id: s.id, - user_id: s.user_id, - organization_id: s.organization_id, - ip_address: s.ip_address, - user_agent: s.user_agent, - created_at: s.created_at, - updated_at: s.updated_at, - }; + return formatEntity(s); } export function formatEmailVerification(ev: WorkOSEmailVerification): Record { - return { - object: 'email_verification', - id: ev.id, - user_id: ev.user_id, - email: ev.email, - code: ev.code, - expires_at: ev.expires_at, - created_at: ev.created_at, - updated_at: ev.updated_at, - }; + return formatEntity(ev); } export function formatPasswordReset(pr: WorkOSPasswordReset): Record { - return { - object: 'password_reset', - id: pr.id, - user_id: pr.user_id, - email: pr.email, - token: pr.token, - expires_at: pr.expires_at, - created_at: pr.created_at, - updated_at: pr.updated_at, - }; + return formatEntity(pr); } export function formatMagicAuth(ma: WorkOSMagicAuth): Record { - return { - object: 'magic_auth', - id: ma.id, - user_id: ma.user_id, - email: ma.email, - code: ma.code, - expires_at: ma.expires_at, - created_at: ma.created_at, - updated_at: ma.updated_at, - }; + return formatEntity(ma); } export function formatAuthFactor(f: WorkOSAuthenticationFactor): Record { - return { - object: 'authentication_factor', - id: f.id, - user_id: f.user_id, - type: f.type, - totp: f.totp, - created_at: f.created_at, - updated_at: f.updated_at, - }; + return formatEntity(f); } export function formatIdentity(i: WorkOSIdentity): Record { - return { - object: 'identity', - id: i.id, - user_id: i.user_id, - provider: i.provider, - provider_id: i.provider_id, - type: i.type, - created_at: i.created_at, - updated_at: i.updated_at, - }; + return formatEntity(i); } export function generateVerificationToken(): string { @@ -207,118 +145,35 @@ export function isExpired(expiresAt: string): boolean { } export function formatConnection(conn: WorkOSConnection): Record { - return { - object: 'connection', - id: conn.id, - organization_id: conn.organization_id, - connection_type: conn.connection_type, - name: conn.name, - state: conn.state, - domains: conn.domains, - created_at: conn.created_at, - updated_at: conn.updated_at, - }; + return formatEntity(conn); } export function formatSSOProfile(p: WorkOSSSOProfile): Record { - return { - object: 'profile', - id: p.id, - connection_id: p.connection_id, - connection_type: p.connection_type, - organization_id: p.organization_id, - idp_id: p.idp_id, - email: p.email, - first_name: p.first_name, - last_name: p.last_name, - groups: p.groups, - raw_attributes: p.raw_attributes, - created_at: p.created_at, - updated_at: p.updated_at, - }; + return formatEntity(p); } export function formatPipeConnection(pc: WorkOSPipeConnection): Record { - return { - object: 'pipe_connection', - id: pc.id, - user_id: pc.user_id, - provider: pc.provider, - scopes: pc.scopes, - status: pc.status, - external_account_id: pc.external_account_id, - created_at: pc.created_at, - updated_at: pc.updated_at, - }; + return formatEntity(pc); } export function formatInvitation(inv: WorkOSInvitation): Record { - return { - object: 'invitation', - id: inv.id, - email: inv.email, - state: inv.state, - token: inv.token, - accept_invitation_url: inv.accept_invitation_url, - organization_id: inv.organization_id, - inviter_user_id: inv.inviter_user_id, - role_slug: inv.role_slug, - expires_at: inv.expires_at, - created_at: inv.created_at, - updated_at: inv.updated_at, - }; + return formatEntity(inv); } export function formatRedirectUri(r: WorkOSRedirectUri): Record { - return { - object: 'redirect_uri', - id: r.id, - uri: r.uri, - created_at: r.created_at, - updated_at: r.updated_at, - }; + return formatEntity(r); } export function formatCorsOrigin(o: WorkOSCorsOrigin): Record { - return { - object: 'cors_origin', - id: o.id, - origin: o.origin, - created_at: o.created_at, - updated_at: o.updated_at, - }; + return formatEntity(o); } export function formatAuthorizedApplication(a: WorkOSAuthorizedApplication): Record { - return { - object: 'authorized_application', - id: a.id, - user_id: a.user_id, - name: a.name, - redirect_uri: a.redirect_uri, - created_at: a.created_at, - updated_at: a.updated_at, - }; + return formatEntity(a); } export function formatConnectedAccount(a: WorkOSConnectedAccount): Record { - return { - object: 'connected_account', - id: a.id, - user_id: a.user_id, - provider: a.provider, - provider_id: a.provider_id, - created_at: a.created_at, - updated_at: a.updated_at, - }; -} - -export function parseListParams(url: URL) { - const limit = Math.max(1, Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 100)); - const order = (url.searchParams.get('order') as 'asc' | 'desc') ?? 'desc'; - const before = url.searchParams.get('before') ?? undefined; - const after = url.searchParams.get('after') ?? undefined; - return { limit, order, before, after }; + return formatEntity(a); } /** Allowed redirect URI hosts for the emulator's authorize endpoints. */ @@ -344,68 +199,26 @@ export function assertLocalRedirectUri(uri: string): void { } } +const AUTH_CHALLENGE_EXCLUDE = new Set([...INTERNAL_FIELDS, 'code']); + export function formatAuthChallenge(c: WorkOSAuthenticationChallenge): Record { - return { - object: 'authentication_challenge', - id: c.id, - user_id: c.user_id, - factor_id: c.factor_id, - expires_at: c.expires_at, - created_at: c.created_at, - updated_at: c.updated_at, - }; + return formatEntity(c, { exclude: AUTH_CHALLENGE_EXCLUDE }); } export function formatRole(role: WorkOSRole): Record { - return { - object: 'role', - id: role.id, - slug: role.slug, - name: role.name, - description: role.description, - type: role.type, - organization_id: role.organization_id, - is_default_role: role.is_default_role, - priority: role.priority, - created_at: role.created_at, - updated_at: role.updated_at, - }; + return formatEntity(role); } export function formatPermission(p: WorkOSPermission): Record { - return { - object: 'permission', - id: p.id, - slug: p.slug, - name: p.name, - description: p.description, - created_at: p.created_at, - updated_at: p.updated_at, - }; + return formatEntity(p); } export function formatAuthorizationResource(r: WorkOSAuthorizationResource): Record { - return { - object: 'authorization_resource', - id: r.id, - resource_type_slug: r.resource_type_slug, - external_id: r.external_id, - organization_id: r.organization_id, - metadata: r.metadata, - created_at: r.created_at, - updated_at: r.updated_at, - }; + return formatEntity(r); } export function formatRoleAssignment(ra: WorkOSRoleAssignment): Record { - return { - object: 'role_assignment', - id: ra.id, - organization_membership_id: ra.organization_membership_id, - role_id: ra.role_id, - created_at: ra.created_at, - updated_at: ra.updated_at, - }; + return formatEntity(ra); } export function formatDeviceAuthorization(d: WorkOSDeviceAuthorization): Record { @@ -421,167 +234,57 @@ export function formatDeviceAuthorization(d: WorkOSDeviceAuthorization): Record< // --- Phase 4: CRUD Domain formatters --- export function formatDirectory(d: WorkOSDirectory): Record { - return { - object: 'directory', - id: d.id, - name: d.name, - organization_id: d.organization_id, - domain: d.domain, - type: d.type, - state: d.state, - external_key: d.external_key, - created_at: d.created_at, - updated_at: d.updated_at, - }; + return formatEntity(d); } export function formatDirectoryUser(u: WorkOSDirectoryUser): Record { - return { - object: 'directory_user', - id: u.id, - directory_id: u.directory_id, - organization_id: u.organization_id, - idp_id: u.idp_id, - first_name: u.first_name, - last_name: u.last_name, - email: u.email, - username: u.username, - state: u.state, - role: u.role, - custom_attributes: u.custom_attributes, - raw_attributes: u.raw_attributes, - groups: u.groups, - created_at: u.created_at, - updated_at: u.updated_at, - }; + return formatEntity(u); } export function formatDirectoryGroup(g: WorkOSDirectoryGroup): Record { - return { - object: 'directory_group', - id: g.id, - directory_id: g.directory_id, - organization_id: g.organization_id, - idp_id: g.idp_id, - name: g.name, - raw_attributes: g.raw_attributes, - created_at: g.created_at, - updated_at: g.updated_at, - }; + return formatEntity(g); } export function formatAuditLogAction(a: WorkOSAuditLogAction): Record { - return { - object: 'audit_log_action', - id: a.id, - name: a.name, - description: a.description, - condition: a.condition, - created_at: a.created_at, - updated_at: a.updated_at, - }; + return formatEntity(a); } export function formatAuditLogEvent(e: WorkOSAuditLogEvent): Record { - return { - object: 'audit_log_event', - id: e.id, - organization_id: e.organization_id, - action: e.action, - actor: e.actor, - targets: e.targets, - metadata: e.metadata, - occurred_at: e.occurred_at, - created_at: e.created_at, - updated_at: e.updated_at, - }; + return formatEntity(e); } export function formatAuditLogExport(ex: WorkOSAuditLogExport): Record { - return { - object: 'audit_log_export', - id: ex.id, - organization_id: ex.organization_id, - state: ex.state, - url: ex.url, - filters: ex.filters, - created_at: ex.created_at, - updated_at: ex.updated_at, - }; + return formatEntity(ex); } export function formatFeatureFlag(f: WorkOSFeatureFlag): Record { - return { - object: 'feature_flag', - id: f.id, - slug: f.slug, - name: f.name, - description: f.description, - type: f.type, - default_value: f.default_value, - enabled: f.enabled, - created_at: f.created_at, - updated_at: f.updated_at, - }; + return formatEntity(f); } export function formatConnectApplication(a: WorkOSConnectApplication): Record { - return { - object: 'connect_application', - id: a.id, - name: a.name, - redirect_uris: a.redirect_uris, - client_id: a.client_id, - logo_url: a.logo_url, - created_at: a.created_at, - updated_at: a.updated_at, - }; + return formatEntity(a); } +const CLIENT_SECRET_EXCLUDE = new Set([...INTERNAL_FIELDS, 'value']); + export function formatClientSecret(s: WorkOSClientSecret): Record { - return { - object: 'client_secret', - id: s.id, - application_id: s.application_id, - last_four: s.last_four, - created_at: s.created_at, - updated_at: s.updated_at, - }; + return formatEntity(s, { exclude: CLIENT_SECRET_EXCLUDE }); } export function formatRadarAttempt(a: WorkOSRadarAttempt): Record { - return { - object: 'radar_attempt', - id: a.id, - user_id: a.user_id, - ip_address: a.ip_address, - user_agent: a.user_agent, - verdict: a.verdict, - signals: a.signals, - created_at: a.created_at, - updated_at: a.updated_at, - }; + return formatEntity(a); } +const API_KEY_EXCLUDE = new Set([...INTERNAL_FIELDS, 'key', 'environment']); + export function formatApiKeyRecord(k: WorkOSApiKey): Record { - return { - object: 'api_key', - id: k.id, - name: k.name, - created_at: k.created_at, - updated_at: k.updated_at, - }; + return formatEntity(k, { exclude: API_KEY_EXCLUDE }); } +const EVENT_EXCLUDE = new Set([...INTERNAL_FIELDS, 'updated_at']); + export function formatEvent(e: WorkOSEvent): Record { - return { - object: 'event', - id: e.id, - event: e.event, - data: e.data, - environment_id: e.environment_id, - created_at: e.created_at, - }; + return formatEntity(e, { exclude: EVENT_EXCLUDE }); } export function formatWebhookEndpoint( diff --git a/src/emulate/workos/index.ts b/src/emulate/workos/index.ts index 5f2e827..19090d8 100644 --- a/src/emulate/workos/index.ts +++ b/src/emulate/workos/index.ts @@ -37,6 +37,7 @@ import { dataIntegrationRoutes } from './routes/data-integrations.js'; import { webhookEndpointRoutes } from './routes/webhook-endpoints.js'; import { eventRoutes } from './routes/events.js'; import { EventBus } from './event-bus.js'; +import { STORE_KEYS, EVENTS } from './constants.js'; import { generateVerificationToken, hashPassword, @@ -367,69 +368,69 @@ export const workosPlugin: ServicePlugin = { // Set up event bus with collection hooks (Option A from spec) // Store on ctx.store for route-level access (hybrid Option A+B for action events) const eventBus = new EventBus(ctx.store); - ctx.store.setData('eventBus', eventBus); + ctx.store.setData(STORE_KEYS.eventBus, eventBus); const ws = getWorkOSStore(ctx.store); ws.users.setHooks({ - onInsert: (u) => eventBus.emit({ event: 'user.created', data: formatUser(u) }), - onUpdate: (u) => eventBus.emit({ event: 'user.updated', data: formatUser(u) }), - onDelete: (u) => eventBus.emit({ event: 'user.deleted', data: formatUser(u) }), + onInsert: (u) => eventBus.emit({ event: EVENTS.userCreated, data: formatUser(u) }), + onUpdate: (u) => eventBus.emit({ event: EVENTS.userUpdated, data: formatUser(u) }), + onDelete: (u) => eventBus.emit({ event: EVENTS.userDeleted, data: formatUser(u) }), }); ws.organizations.setHooks({ - onInsert: (o) => eventBus.emit({ event: 'organization.created', data: formatOrganization(o, ws) }), - onUpdate: (o) => eventBus.emit({ event: 'organization.updated', data: formatOrganization(o, ws) }), - onDelete: (o) => eventBus.emit({ event: 'organization.deleted', data: formatOrganization(o, ws) }), + onInsert: (o) => eventBus.emit({ event: EVENTS.organizationCreated, data: formatOrganization(o, ws) }), + onUpdate: (o) => eventBus.emit({ event: EVENTS.organizationUpdated, data: formatOrganization(o, ws) }), + onDelete: (o) => eventBus.emit({ event: EVENTS.organizationDeleted, data: formatOrganization(o, ws) }), }); ws.organizationDomains.setHooks({ - onInsert: (d) => eventBus.emit({ event: 'organization_domain.created', data: formatDomain(d) }), + onInsert: (d) => eventBus.emit({ event: EVENTS.organizationDomainCreated, data: formatDomain(d) }), onUpdate: (d) => eventBus.emit({ - event: d.state === 'verified' ? 'organization_domain.verified' : 'organization_domain.updated', + event: d.state === 'verified' ? EVENTS.organizationDomainVerified : EVENTS.organizationDomainUpdated, data: formatDomain(d), }), - onDelete: (d) => eventBus.emit({ event: 'organization_domain.deleted', data: formatDomain(d) }), + onDelete: (d) => eventBus.emit({ event: EVENTS.organizationDomainDeleted, data: formatDomain(d) }), }); ws.organizationMemberships.setHooks({ - onInsert: (m) => eventBus.emit({ event: 'organization_membership.created', data: formatMembership(m) }), - onUpdate: (m) => eventBus.emit({ event: 'organization_membership.updated', data: formatMembership(m) }), - onDelete: (m) => eventBus.emit({ event: 'organization_membership.deleted', data: formatMembership(m) }), + onInsert: (m) => eventBus.emit({ event: EVENTS.organizationMembershipCreated, data: formatMembership(m) }), + onUpdate: (m) => eventBus.emit({ event: EVENTS.organizationMembershipUpdated, data: formatMembership(m) }), + onDelete: (m) => eventBus.emit({ event: EVENTS.organizationMembershipDeleted, data: formatMembership(m) }), }); ws.connections.setHooks({ - onInsert: (c) => eventBus.emit({ event: 'connection.created', data: formatConnection(c) }), - onUpdate: (c) => eventBus.emit({ event: 'connection.updated', data: formatConnection(c) }), - onDelete: (c) => eventBus.emit({ event: 'connection.deleted', data: formatConnection(c) }), + onInsert: (c) => eventBus.emit({ event: EVENTS.connectionCreated, data: formatConnection(c) }), + onUpdate: (c) => eventBus.emit({ event: EVENTS.connectionUpdated, data: formatConnection(c) }), + onDelete: (c) => eventBus.emit({ event: EVENTS.connectionDeleted, data: formatConnection(c) }), }); ws.sessions.setHooks({ - onInsert: (s) => eventBus.emit({ event: 'session.created', data: formatSession(s) }), - onDelete: (s) => eventBus.emit({ event: 'session.revoked', data: formatSession(s) }), + onInsert: (s) => eventBus.emit({ event: EVENTS.sessionCreated, data: formatSession(s) }), + onDelete: (s) => eventBus.emit({ event: EVENTS.sessionRevoked, data: formatSession(s) }), }); ws.invitations.setHooks({ - onInsert: (i) => eventBus.emit({ event: 'invitation.created', data: formatInvitation(i) }), + onInsert: (i) => eventBus.emit({ event: EVENTS.invitationCreated, data: formatInvitation(i) }), }); ws.roles.setHooks({ - onInsert: (r) => eventBus.emit({ event: 'role.created', data: formatRole(r) }), - onUpdate: (r) => eventBus.emit({ event: 'role.updated', data: formatRole(r) }), - onDelete: (r) => eventBus.emit({ event: 'role.deleted', data: formatRole(r) }), + onInsert: (r) => eventBus.emit({ event: EVENTS.roleCreated, data: formatRole(r) }), + onUpdate: (r) => eventBus.emit({ event: EVENTS.roleUpdated, data: formatRole(r) }), + onDelete: (r) => eventBus.emit({ event: EVENTS.roleDeleted, data: formatRole(r) }), }); ws.permissions.setHooks({ - onInsert: (p) => eventBus.emit({ event: 'permission.created', data: formatPermission(p) }), - onUpdate: (p) => eventBus.emit({ event: 'permission.updated', data: formatPermission(p) }), - onDelete: (p) => eventBus.emit({ event: 'permission.deleted', data: formatPermission(p) }), + onInsert: (p) => eventBus.emit({ event: EVENTS.permissionCreated, data: formatPermission(p) }), + onUpdate: (p) => eventBus.emit({ event: EVENTS.permissionUpdated, data: formatPermission(p) }), + onDelete: (p) => eventBus.emit({ event: EVENTS.permissionDeleted, data: formatPermission(p) }), }); ws.directories.setHooks({ - onInsert: (d) => eventBus.emit({ event: 'directory.created', data: formatDirectory(d) }), - onUpdate: (d) => eventBus.emit({ event: 'directory.updated', data: formatDirectory(d) }), - onDelete: (d) => eventBus.emit({ event: 'directory.deleted', data: formatDirectory(d) }), + onInsert: (d) => eventBus.emit({ event: EVENTS.directoryCreated, data: formatDirectory(d) }), + onUpdate: (d) => eventBus.emit({ event: EVENTS.directoryUpdated, data: formatDirectory(d) }), + onDelete: (d) => eventBus.emit({ event: EVENTS.directoryDeleted, data: formatDirectory(d) }), }); ws.directoryUsers.setHooks({ - onInsert: (u) => eventBus.emit({ event: 'directory_user.created', data: formatDirectoryUser(u) }), - onUpdate: (u) => eventBus.emit({ event: 'directory_user.updated', data: formatDirectoryUser(u) }), - onDelete: (u) => eventBus.emit({ event: 'directory_user.deleted', data: formatDirectoryUser(u) }), + onInsert: (u) => eventBus.emit({ event: EVENTS.directoryUserCreated, data: formatDirectoryUser(u) }), + onUpdate: (u) => eventBus.emit({ event: EVENTS.directoryUserUpdated, data: formatDirectoryUser(u) }), + onDelete: (u) => eventBus.emit({ event: EVENTS.directoryUserDeleted, data: formatDirectoryUser(u) }), }); ws.directoryGroups.setHooks({ - onInsert: (g) => eventBus.emit({ event: 'directory_group.created', data: formatDirectoryGroup(g) }), - onUpdate: (g) => eventBus.emit({ event: 'directory_group.updated', data: formatDirectoryGroup(g) }), - onDelete: (g) => eventBus.emit({ event: 'directory_group.deleted', data: formatDirectoryGroup(g) }), + onInsert: (g) => eventBus.emit({ event: EVENTS.directoryGroupCreated, data: formatDirectoryGroup(g) }), + onUpdate: (g) => eventBus.emit({ event: EVENTS.directoryGroupUpdated, data: formatDirectoryGroup(g) }), + onDelete: (g) => eventBus.emit({ event: EVENTS.directoryGroupDeleted, data: formatDirectoryGroup(g) }), }); }, seed(_store: Store, _baseUrl: string): void { diff --git a/src/emulate/workos/routes/api-keys.ts b/src/emulate/workos/routes/api-keys.ts index 8d21916..b699b1d 100644 --- a/src/emulate/workos/routes/api-keys.ts +++ b/src/emulate/workos/routes/api-keys.ts @@ -1,7 +1,8 @@ -import { type RouteContext, notFound, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatApiKeyRecord, parseListParams } from '../helpers.js'; +import { formatApiKeyRecord, formatListResponse } from '../helpers.js'; import type { ApiKeyMap } from '../../core/index.js'; +import { STORE_KEYS } from '../constants.js'; export function apiKeyRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -11,7 +12,7 @@ export function apiKeyRoutes(ctx: RouteContext): void { app.post('/api_keys/validations', async (c) => { const body = await parseJsonBody(c); const key = body.key as string | undefined; - const apiKeyMap = store.getData('apiKeyMap') ?? {}; + const apiKeyMap = store.getData(STORE_KEYS.apiKeyMap) ?? {}; const valid = !!key && key in apiKeyMap; return c.json({ valid }); }); @@ -29,10 +30,6 @@ export function apiKeyRoutes(ctx: RouteContext): void { const url = new URL(c.req.url); const params = parseListParams(url); const result = ws.apiKeyRecords.list({ ...params }); - return c.json({ - object: 'list', - data: result.data.map(formatApiKeyRecord), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatApiKeyRecord)); }); } diff --git a/src/emulate/workos/routes/audit-logs.ts b/src/emulate/workos/routes/audit-logs.ts index 9b04f0c..0721fc7 100644 --- a/src/emulate/workos/routes/audit-logs.ts +++ b/src/emulate/workos/routes/audit-logs.ts @@ -1,6 +1,7 @@ -import { type RouteContext, notFound, parseJsonBody, validationError } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, validationError, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatAuditLogAction, formatAuditLogEvent, formatAuditLogExport, parseListParams } from '../helpers.js'; +import { formatAuditLogAction, formatAuditLogEvent, formatAuditLogExport, formatListResponse } from '../helpers.js'; +import { STORE_KEY_PREFIXES } from '../constants.js'; export function auditLogRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -11,11 +12,7 @@ export function auditLogRoutes(ctx: RouteContext): void { const url = new URL(c.req.url); const params = parseListParams(url); const result = ws.auditLogActions.list({ ...params }); - return c.json({ - object: 'list', - data: result.data.map(formatAuditLogAction), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatAuditLogAction)); }); // Create/update action schema @@ -27,7 +24,7 @@ export function auditLogRoutes(ctx: RouteContext): void { let action = ws.auditLogActions.findOneBy('name', actionName); if (action) { // Store schema in store data keyed by action name - store.setData(`audit_schema_${actionName}`, body); + store.setData(`${STORE_KEY_PREFIXES.auditSchema}${actionName}`, body); return c.json(formatAuditLogAction(action)); } @@ -37,7 +34,7 @@ export function auditLogRoutes(ctx: RouteContext): void { description: null, condition: null, }); - store.setData(`audit_schema_${actionName}`, body); + store.setData(`${STORE_KEY_PREFIXES.auditSchema}${actionName}`, body); return c.json(formatAuditLogAction(action), 201); }); diff --git a/src/emulate/workos/routes/auth.ts b/src/emulate/workos/routes/auth.ts index 3e8e925..b7cd14c 100644 --- a/src/emulate/workos/routes/auth.ts +++ b/src/emulate/workos/routes/auth.ts @@ -11,6 +11,7 @@ import { sealSession, } from '../helpers.js'; import type { EventBus } from '../event-bus.js'; +import { STORE_KEYS, STORE_KEY_PREFIXES } from '../constants.js'; interface PendingAuth { user_id: string; @@ -246,7 +247,7 @@ export function authRoutes(ctx: RouteContext): void { ); } - const pending = store.getData(`pending_auth:${pendingToken}`); + const pending = store.getData(`${STORE_KEY_PREFIXES.pendingAuth}${pendingToken}`); if (!pending) { throw new WorkOSApiError(400, 'Invalid pending authentication token', 'invalid_pending_authentication_token'); } @@ -266,7 +267,7 @@ export function authRoutes(ctx: RouteContext): void { } ws.authChallenges.delete(challenge.id); - store.setData(`pending_auth:${pendingToken}`, undefined); + store.setData(`${STORE_KEY_PREFIXES.pendingAuth}${pendingToken}`, undefined); user = ws.users.get(pending.user_id); organizationId = pending.organization_id; @@ -286,7 +287,7 @@ export function authRoutes(ctx: RouteContext): void { ); } - const pending = store.getData(`pending_auth:${pendingToken}`); + const pending = store.getData(`${STORE_KEY_PREFIXES.pendingAuth}${pendingToken}`); if (!pending) { throw new WorkOSApiError(400, 'Invalid pending authentication token', 'invalid_pending_authentication_token'); } @@ -294,7 +295,7 @@ export function authRoutes(ctx: RouteContext): void { const org = ws.organizations.get(orgId); if (!org) throw notFound('Organization'); - store.setData(`pending_auth:${pendingToken}`, undefined); + store.setData(`${STORE_KEY_PREFIXES.pendingAuth}${pendingToken}`, undefined); user = ws.users.get(pending.user_id); organizationId = orgId; @@ -397,7 +398,7 @@ export function authRoutes(ctx: RouteContext): void { : null; // Emit authentication event (hybrid Option B for action-specific events) - const eventBus = store.getData('eventBus'); + const eventBus = store.getData(STORE_KEYS.eventBus); if (eventBus) { const authEventType = `authentication.${authMethod.toLowerCase()}_succeeded`; eventBus.emit({ diff --git a/src/emulate/workos/routes/authorization-checks.ts b/src/emulate/workos/routes/authorization-checks.ts index 4368e5e..411114b 100644 --- a/src/emulate/workos/routes/authorization-checks.ts +++ b/src/emulate/workos/routes/authorization-checks.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatRoleAssignment, formatAuthorizationResource, parseListParams } from '../helpers.js'; +import { formatRoleAssignment, formatAuthorizationResource, formatListResponse } from '../helpers.js'; /** * Gather all permission slugs for a given membership: @@ -74,11 +74,7 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { filter: (r) => r.organization_id === membership.organization_id, }); - return c.json({ - object: 'list', - data: result.data.map(formatAuthorizationResource), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatAuthorizationResource)); }); // List role assignments for a membership @@ -95,11 +91,7 @@ export function authorizationCheckRoutes(ctx: RouteContext): void { filter: (ra) => ra.organization_membership_id === membershipId, }); - return c.json({ - object: 'list', - data: result.data.map(formatRoleAssignment), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatRoleAssignment)); }); // Create role assignment diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index 8788d66..7bf2c47 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatRole, formatPermission, parseListParams } from '../helpers.js'; +import { formatRole, formatPermission, formatListResponse } from '../helpers.js'; export function authorizationOrgRoleRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -56,11 +56,7 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { filter: (r) => r.organization_id === orgId && r.type === 'OrganizationRole', }); - return c.json({ - object: 'list', - data: result.data.map(formatRole), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatRole)); }); // Priority ordering — must be registered before :slug routes diff --git a/src/emulate/workos/routes/authorization-permissions.ts b/src/emulate/workos/routes/authorization-permissions.ts index d8e5954..8fadef4 100644 --- a/src/emulate/workos/routes/authorization-permissions.ts +++ b/src/emulate/workos/routes/authorization-permissions.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatPermission, parseListParams } from '../helpers.js'; +import { formatPermission, formatListResponse } from '../helpers.js'; export function authorizationPermissionRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -38,11 +38,7 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { const params = parseListParams(url); const result = ws.permissions.list(params); - return c.json({ - object: 'list', - data: result.data.map(formatPermission), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatPermission)); }); app.get('/authorization/permissions/:slug', (c) => { diff --git a/src/emulate/workos/routes/authorization-resources.ts b/src/emulate/workos/routes/authorization-resources.ts index cbe270f..d78430d 100644 --- a/src/emulate/workos/routes/authorization-resources.ts +++ b/src/emulate/workos/routes/authorization-resources.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatAuthorizationResource, formatMembership, parseListParams } from '../helpers.js'; +import { formatAuthorizationResource, formatMembership, formatListResponse } from '../helpers.js'; export function authorizationResourceRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -49,11 +49,7 @@ export function authorizationResourceRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatAuthorizationResource), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatAuthorizationResource)); }); app.get('/authorization/resources/:resource_id', (c) => { diff --git a/src/emulate/workos/routes/authorization-roles.ts b/src/emulate/workos/routes/authorization-roles.ts index 6bdb9ef..ec2128f 100644 --- a/src/emulate/workos/routes/authorization-roles.ts +++ b/src/emulate/workos/routes/authorization-roles.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatRole, formatPermission, parseListParams } from '../helpers.js'; +import { formatRole, formatPermission, formatListResponse } from '../helpers.js'; export function authorizationRoleRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -47,11 +47,7 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { filter: (r) => r.type === 'EnvironmentRole', }); - return c.json({ - object: 'list', - data: result.data.map(formatRole), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatRole)); }); app.get('/authorization/roles/:slug', (c) => { diff --git a/src/emulate/workos/routes/config.ts b/src/emulate/workos/routes/config.ts index 901caaf..4487d92 100644 --- a/src/emulate/workos/routes/config.ts +++ b/src/emulate/workos/routes/config.ts @@ -1,6 +1,7 @@ import { type RouteContext, parseJsonBody, WorkOSApiError, validationError } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; import { formatRedirectUri, formatCorsOrigin } from '../helpers.js'; +import { STORE_KEYS } from '../constants.js'; export function configRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -47,7 +48,7 @@ export function configRoutes(ctx: RouteContext): void { }); app.get('/user_management/jwt_template', (c) => { - const template = store.getData>('jwt_template') ?? { + const template = store.getData>(STORE_KEYS.jwtTemplate) ?? { object: 'jwt_template', custom_claims: {}, }; @@ -60,7 +61,7 @@ export function configRoutes(ctx: RouteContext): void { object: 'jwt_template', custom_claims: (body.custom_claims as Record) ?? {}, }; - store.setData('jwt_template', template); + store.setData(STORE_KEYS.jwtTemplate, template); return c.json(template); }); } diff --git a/src/emulate/workos/routes/connect.ts b/src/emulate/workos/routes/connect.ts index 9fb738e..7d63dfe 100644 --- a/src/emulate/workos/routes/connect.ts +++ b/src/emulate/workos/routes/connect.ts @@ -1,12 +1,7 @@ -import { type RouteContext, notFound, parseJsonBody, validationError } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, validationError, parseListParams } from '../../core/index.js'; import { generateId } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { - formatConnectApplication, - formatClientSecret, - parseListParams, - generateVerificationToken, -} from '../helpers.js'; +import { formatConnectApplication, formatClientSecret, generateVerificationToken, formatListResponse } from '../helpers.js'; export function connectRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -17,11 +12,7 @@ export function connectRoutes(ctx: RouteContext): void { const url = new URL(c.req.url); const params = parseListParams(url); const result = ws.connectApplications.list({ ...params }); - return c.json({ - object: 'list', - data: result.data.map(formatConnectApplication), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatConnectApplication)); }); // Create application diff --git a/src/emulate/workos/routes/connections.ts b/src/emulate/workos/routes/connections.ts index cee6e3e..5080ed2 100644 --- a/src/emulate/workos/routes/connections.ts +++ b/src/emulate/workos/routes/connections.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, parseJsonBody, generateId } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, generateId, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatConnection, parseListParams } from '../helpers.js'; +import { formatConnection, formatListResponse } from '../helpers.js'; import type { WorkOSConnectionType } from '../entities.js'; export function connectionRoutes(ctx: RouteContext): void { @@ -55,11 +55,7 @@ export function connectionRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatConnection), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatConnection)); }); app.get('/connections/:id', (c) => { diff --git a/src/emulate/workos/routes/directories.ts b/src/emulate/workos/routes/directories.ts index f7698f5..46e08e0 100644 --- a/src/emulate/workos/routes/directories.ts +++ b/src/emulate/workos/routes/directories.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound } from '../../core/index.js'; +import { type RouteContext, notFound, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatDirectory, formatDirectoryUser, formatDirectoryGroup, parseListParams } from '../helpers.js'; +import { formatDirectory, formatDirectoryUser, formatDirectoryGroup, formatListResponse } from '../helpers.js'; export function directoryRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -22,11 +22,7 @@ export function directoryRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatDirectory), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatDirectory)); }); // Get directory @@ -67,11 +63,7 @@ export function directoryRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatDirectoryUser), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatDirectoryUser)); }); // Get directory user @@ -95,11 +87,7 @@ export function directoryRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatDirectoryGroup), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatDirectoryGroup)); }); // Get directory group diff --git a/src/emulate/workos/routes/events.ts b/src/emulate/workos/routes/events.ts index 88ffe96..fdfad77 100644 --- a/src/emulate/workos/routes/events.ts +++ b/src/emulate/workos/routes/events.ts @@ -1,6 +1,6 @@ -import { type RouteContext } from '../../core/index.js'; +import { type RouteContext, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatEvent, parseListParams } from '../helpers.js'; +import { formatEvent, formatListResponse } from '../helpers.js'; export function eventRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -16,10 +16,6 @@ export function eventRoutes(ctx: RouteContext): void { filter: eventTypes.length > 0 ? (e) => eventTypes.includes(e.event) : undefined, }); - return c.json({ - object: 'list', - data: result.data.map(formatEvent), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatEvent)); }); } diff --git a/src/emulate/workos/routes/feature-flags.ts b/src/emulate/workos/routes/feature-flags.ts index 02d2bfc..f51c99d 100644 --- a/src/emulate/workos/routes/feature-flags.ts +++ b/src/emulate/workos/routes/feature-flags.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatFeatureFlag, parseListParams } from '../helpers.js'; +import { formatFeatureFlag, formatListResponse } from '../helpers.js'; export function featureFlagRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -11,11 +11,7 @@ export function featureFlagRoutes(ctx: RouteContext): void { const url = new URL(c.req.url); const params = parseListParams(url); const result = ws.featureFlags.list({ ...params }); - return c.json({ - object: 'list', - data: result.data.map(formatFeatureFlag), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatFeatureFlag)); }); // Get flag by slug diff --git a/src/emulate/workos/routes/invitations.ts b/src/emulate/workos/routes/invitations.ts index 4f1db27..df21a8e 100644 --- a/src/emulate/workos/routes/invitations.ts +++ b/src/emulate/workos/routes/invitations.ts @@ -1,7 +1,8 @@ -import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatInvitation, generateVerificationToken, expiresIn, parseListParams } from '../helpers.js'; +import { formatInvitation, generateVerificationToken, expiresIn, formatListResponse } from '../helpers.js'; import type { EventBus } from '../event-bus.js'; +import { STORE_KEYS, EVENTS } from '../constants.js'; export function invitationRoutes(ctx: RouteContext): void { const { app, store, baseUrl } = ctx; @@ -45,11 +46,7 @@ export function invitationRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatInvitation), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatInvitation)); }); app.get('/user_management/invitations/by_token/:token', (c) => { @@ -73,8 +70,8 @@ export function invitationRoutes(ctx: RouteContext): void { } ws.invitations.update(inv.id, { state: 'accepted' }); - const eventBus = store.getData('eventBus'); - eventBus?.emit({ event: 'invitation.accepted', data: formatInvitation(ws.invitations.get(inv.id)!) }); + const eventBus = store.getData(STORE_KEYS.eventBus); + eventBus?.emit({ event: EVENTS.invitationAccepted, data: formatInvitation(ws.invitations.get(inv.id)!) }); // Create org membership if invitation has an organization if (inv.organization_id) { @@ -105,8 +102,8 @@ export function invitationRoutes(ctx: RouteContext): void { } ws.invitations.update(inv.id, { state: 'revoked' }); - const eventBus = store.getData('eventBus'); - eventBus?.emit({ event: 'invitation.revoked', data: formatInvitation(ws.invitations.get(inv.id)!) }); + const eventBus = store.getData(STORE_KEYS.eventBus); + eventBus?.emit({ event: EVENTS.invitationRevoked, data: formatInvitation(ws.invitations.get(inv.id)!) }); const updated = ws.invitations.get(inv.id)!; return c.json(formatInvitation(updated)); }); @@ -123,8 +120,8 @@ export function invitationRoutes(ctx: RouteContext): void { state: 'pending', }); - const eventBus = store.getData('eventBus'); - eventBus?.emit({ event: 'invitation.resent', data: formatInvitation(ws.invitations.get(inv.id)!) }); + const eventBus = store.getData(STORE_KEYS.eventBus); + eventBus?.emit({ event: EVENTS.invitationResent, data: formatInvitation(ws.invitations.get(inv.id)!) }); const updated = ws.invitations.get(inv.id)!; return c.json(formatInvitation(updated)); }); diff --git a/src/emulate/workos/routes/memberships.ts b/src/emulate/workos/routes/memberships.ts index 1864685..bd1144b 100644 --- a/src/emulate/workos/routes/memberships.ts +++ b/src/emulate/workos/routes/memberships.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatMembership, parseListParams } from '../helpers.js'; +import { formatMembership, formatListResponse } from '../helpers.js'; export function membershipRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -60,11 +60,7 @@ export function membershipRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatMembership), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatMembership)); }); app.get('/user_management/organization_memberships/:id', (c) => { diff --git a/src/emulate/workos/routes/organizations.ts b/src/emulate/workos/routes/organizations.ts index 09a0438..172edb3 100644 --- a/src/emulate/workos/routes/organizations.ts +++ b/src/emulate/workos/routes/organizations.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatOrganization, generateVerificationToken, parseListParams } from '../helpers.js'; +import { formatOrganization, generateVerificationToken, formatListResponse } from '../helpers.js'; export function organizationRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -61,11 +61,7 @@ export function organizationRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map((org) => formatOrganization(org, ws)), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, (org) => formatOrganization(org, ws))); }); app.get('/organizations/:id', (c) => { diff --git a/src/emulate/workos/routes/pipes.ts b/src/emulate/workos/routes/pipes.ts index d9de84f..59d7a86 100644 --- a/src/emulate/workos/routes/pipes.ts +++ b/src/emulate/workos/routes/pipes.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatPipeConnection, parseListParams } from '../helpers.js'; +import { formatPipeConnection, formatListResponse } from '../helpers.js'; import type { PipeProvider } from '../entities.js'; const VALID_PROVIDERS: PipeProvider[] = ['github', 'slack', 'google', 'salesforce']; @@ -54,11 +54,7 @@ export function pipeRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatPipeConnection), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatPipeConnection)); }); app.get('/pipes/connections/:id', (c) => { diff --git a/src/emulate/workos/routes/radar.ts b/src/emulate/workos/routes/radar.ts index 984407a..133b708 100644 --- a/src/emulate/workos/routes/radar.ts +++ b/src/emulate/workos/routes/radar.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatRadarAttempt, parseListParams } from '../helpers.js'; +import { formatRadarAttempt, formatListResponse } from '../helpers.js'; export function radarRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -11,11 +11,7 @@ export function radarRoutes(ctx: RouteContext): void { const url = new URL(c.req.url); const params = parseListParams(url); const result = ws.radarAttempts.list({ ...params }); - return c.json({ - object: 'list', - data: result.data.map(formatRadarAttempt), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatRadarAttempt)); }); // Get attempt diff --git a/src/emulate/workos/routes/sso.ts b/src/emulate/workos/routes/sso.ts index dd49b36..b912775 100644 --- a/src/emulate/workos/routes/sso.ts +++ b/src/emulate/workos/routes/sso.ts @@ -2,6 +2,7 @@ import { type RouteContext, parseJsonBody, WorkOSApiError, generateId } from '.. import { getWorkOSStore } from '../store.js'; import { formatSSOProfile, expiresIn, isExpired, assertLocalRedirectUri } from '../helpers.js'; import type { WorkOSConnection } from '../entities.js'; +import { STORE_KEY_PREFIXES } from '../constants.js'; export function ssoRoutes(ctx: RouteContext): void { const { app, store, jwt } = ctx; @@ -104,7 +105,7 @@ export function ssoRoutes(ctx: RouteContext): void { org_id: auth.organization_id, }); - store.setData(`sso_token:${accessToken}`, profile.id); + store.setData(`${STORE_KEY_PREFIXES.ssoToken}${accessToken}`, profile.id); return c.json({ profile: formatSSOProfile(profile), @@ -119,7 +120,7 @@ export function ssoRoutes(ctx: RouteContext): void { } const token = authHeader.replace(/^Bearer\s+/i, '').trim(); - const profileId = store.getData(`sso_token:${token}`); + const profileId = store.getData(`${STORE_KEY_PREFIXES.ssoToken}${token}`); if (!profileId) { try { const payload = jwt.verify(token); @@ -157,7 +158,7 @@ export function ssoRoutes(ctx: RouteContext): void { } const logoutToken = generateId('sso_logout'); - store.setData(`sso_logout:${logoutToken}`, profile.id); + store.setData(`${STORE_KEY_PREFIXES.ssoLogout}${logoutToken}`, profile.id); return c.json({ logout_token: logoutToken, @@ -174,12 +175,12 @@ export function ssoRoutes(ctx: RouteContext): void { throw new WorkOSApiError(400, 'logout_token is required', 'invalid_request'); } - const profileId = store.getData(`sso_logout:${logoutToken}`); + const profileId = store.getData(`${STORE_KEY_PREFIXES.ssoLogout}${logoutToken}`); if (!profileId) { throw new WorkOSApiError(400, 'Invalid logout token', 'invalid_logout_token'); } - store.setData(`sso_logout:${logoutToken}`, undefined); + store.setData(`${STORE_KEY_PREFIXES.ssoLogout}${logoutToken}`, undefined); return c.json({ success: true }); }); } diff --git a/src/emulate/workos/routes/users.ts b/src/emulate/workos/routes/users.ts index 424eb67..54ea9c9 100644 --- a/src/emulate/workos/routes/users.ts +++ b/src/emulate/workos/routes/users.ts @@ -1,6 +1,6 @@ -import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, WorkOSApiError, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatUser, formatIdentity, hashPassword, parseListParams } from '../helpers.js'; +import { formatUser, formatIdentity, hashPassword, formatListResponse } from '../helpers.js'; export function userRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -57,11 +57,7 @@ export function userRoutes(ctx: RouteContext): void { }, }); - return c.json({ - object: 'list', - data: result.data.map(formatUser), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, formatUser)); }); app.get('/user_management/users/:id', (c) => { diff --git a/src/emulate/workos/routes/webhook-endpoints.ts b/src/emulate/workos/routes/webhook-endpoints.ts index 724c4aa..810a793 100644 --- a/src/emulate/workos/routes/webhook-endpoints.ts +++ b/src/emulate/workos/routes/webhook-endpoints.ts @@ -1,7 +1,7 @@ import { randomBytes } from 'node:crypto'; -import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatWebhookEndpoint, parseListParams } from '../helpers.js'; +import { formatWebhookEndpoint, formatListResponse } from '../helpers.js'; export function webhookEndpointRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -33,11 +33,7 @@ export function webhookEndpointRoutes(ctx: RouteContext): void { const params = parseListParams(url); const result = ws.webhookEndpoints.list(params); - return c.json({ - object: 'list', - data: result.data.map((ep) => formatWebhookEndpoint(ep)), - list_metadata: result.list_metadata, - }); + return c.json(formatListResponse(result, (ep) => formatWebhookEndpoint(ep))); }); app.get('/webhook_endpoints/:id', (c) => { diff --git a/src/emulate/workos/store.ts b/src/emulate/workos/store.ts index 565d116..2eda217 100644 --- a/src/emulate/workos/store.ts +++ b/src/emulate/workos/store.ts @@ -1,4 +1,5 @@ -import { type Store, type Collection } from '../core/index.js'; +import { type Store, type Collection, ID_PREFIXES } from '../core/index.js'; +import { STORE_KEYS } from './constants.js'; import type { WorkOSOrganization, WorkOSOrganizationDomain, @@ -91,111 +92,164 @@ export interface WorkOSStore { webhookEndpoints: Collection; } -const CACHE_KEY = '_workos_store'; - export function getWorkOSStore(store: Store): WorkOSStore { - const cached = store.getData(CACHE_KEY); + const cached = store.getData(STORE_KEYS.workosStore); if (cached) return cached; const ws: WorkOSStore = { - organizations: store.collection('workos.organizations', 'org', ['name', 'external_id']), - organizationDomains: store.collection('workos.organization_domains', 'org_domain', [ - 'organization_id', - 'domain', - ]), - organizationMemberships: store.collection('workos.organization_memberships', 'om', [ - 'organization_id', + organizations: store.collection('workos.organizations', ID_PREFIXES.organization, [ + 'name', + 'external_id', + ]), + organizationDomains: store.collection( + 'workos.organization_domains', + ID_PREFIXES.organization_domain, + ['organization_id', 'domain'], + ), + organizationMemberships: store.collection( + 'workos.organization_memberships', + ID_PREFIXES.organization_membership, + ['organization_id', 'user_id'], + ), + users: store.collection('workos.users', ID_PREFIXES.user, ['email', 'external_id']), + sessions: store.collection('workos.sessions', ID_PREFIXES.session, ['user_id']), + emailVerifications: store.collection( + 'workos.email_verifications', + ID_PREFIXES.email_verification, + ['user_id'], + ), + passwordResets: store.collection( + 'workos.password_resets', + ID_PREFIXES.password_reset, + ['user_id'], + ), + magicAuths: store.collection('workos.magic_auths', ID_PREFIXES.magic_auth, ['user_id']), + authFactors: store.collection( + 'workos.auth_factors', + ID_PREFIXES.authentication_factor, + ['user_id'], + ), + authCodes: store.collection('workos.auth_codes', ID_PREFIXES.authorization_code, [ 'user_id', + 'code', ]), - users: store.collection('workos.users', 'user', ['email', 'external_id']), - sessions: store.collection('workos.sessions', 'session', ['user_id']), - emailVerifications: store.collection('workos.email_verifications', 'email_verification', [ - 'user_id', + identities: store.collection('workos.identities', ID_PREFIXES.identity, ['user_id']), + connections: store.collection('workos.connections', ID_PREFIXES.connection, ['organization_id']), + ssoProfiles: store.collection('workos.sso_profiles', ID_PREFIXES.profile, [ + 'connection_id', + 'email', ]), - passwordResets: store.collection('workos.password_resets', 'password_reset', ['user_id']), - magicAuths: store.collection('workos.magic_auths', 'magic_auth', ['user_id']), - authFactors: store.collection('workos.auth_factors', 'auth_factor', ['user_id']), - authCodes: store.collection('workos.auth_codes', 'auth_code', ['user_id', 'code']), - identities: store.collection('workos.identities', 'identity', ['user_id']), - connections: store.collection('workos.connections', 'conn', ['organization_id']), - ssoProfiles: store.collection('workos.sso_profiles', 'prof', ['connection_id', 'email']), - ssoAuthorizations: store.collection('workos.sso_authorizations', 'sso_auth', ['code']), - pipeConnections: store.collection('workos.pipe_connections', 'pipe_conn', [ + ssoAuthorizations: store.collection( + 'workos.sso_authorizations', + ID_PREFIXES.sso_authorization, + ['code'], + ), + pipeConnections: store.collection('workos.pipe_connections', ID_PREFIXES.pipe_connection, [ 'user_id', 'provider', ]), - refreshTokens: store.collection('workos.refresh_tokens', 'ref', [ + refreshTokens: store.collection('workos.refresh_tokens', ID_PREFIXES.refresh_token, [ 'token', 'user_id', 'session_id', ]), - authChallenges: store.collection('workos.auth_challenges', 'auth_challenge', [ - 'user_id', - 'factor_id', - ]), - deviceAuthorizations: store.collection('workos.device_authorizations', 'dev_auth', [ - 'device_code', - 'user_code', + authChallenges: store.collection( + 'workos.auth_challenges', + ID_PREFIXES.authentication_challenge, + ['user_id', 'factor_id'], + ), + deviceAuthorizations: store.collection( + 'workos.device_authorizations', + ID_PREFIXES.device_authorization, + ['device_code', 'user_code'], + ), + invitations: store.collection('workos.invitations', ID_PREFIXES.invitation, [ + 'email', + 'token', + 'organization_id', ]), - invitations: store.collection('workos.invitations', 'inv', ['email', 'token', 'organization_id']), - redirectUris: store.collection('workos.redirect_uris', 'redir', ['uri']), - corsOrigins: store.collection('workos.cors_origins', 'cors', ['origin']), + redirectUris: store.collection('workos.redirect_uris', ID_PREFIXES.redirect_uri, ['uri']), + corsOrigins: store.collection('workos.cors_origins', ID_PREFIXES.cors_origin, ['origin']), authorizedApplications: store.collection( 'workos.authorized_applications', - 'auth_app', + ID_PREFIXES.authorized_application, ['user_id'], ), - connectedAccounts: store.collection('workos.connected_accounts', 'conn_acct', [ - 'user_id', - 'provider', - ]), - roles: store.collection('workos.roles', 'role', ['slug', 'organization_id']), - permissions: store.collection('workos.permissions', 'perm', ['slug']), - rolePermissions: store.collection('workos.role_permissions', 'rp', [ + connectedAccounts: store.collection( + 'workos.connected_accounts', + ID_PREFIXES.connected_account, + ['user_id', 'provider'], + ), + roles: store.collection('workos.roles', ID_PREFIXES.role, ['slug', 'organization_id']), + permissions: store.collection('workos.permissions', ID_PREFIXES.permission, ['slug']), + rolePermissions: store.collection('workos.role_permissions', ID_PREFIXES.role_permission, [ 'role_id', 'permission_id', ]), authorizationResources: store.collection( 'workos.authorization_resources', - 'auth_res', + ID_PREFIXES.authorization_resource, ['organization_id', 'resource_type_slug'], ), - roleAssignments: store.collection('workos.role_assignments', 'ra', [ - 'organization_membership_id', - 'role_id', - ]), - directories: store.collection('workos.directories', 'directory', ['organization_id']), - directoryUsers: store.collection('workos.directory_users', 'directory_user', [ + roleAssignments: store.collection( + 'workos.role_assignments', + ID_PREFIXES.role_assignment, + ['organization_membership_id', 'role_id'], + ), + directories: store.collection('workos.directories', ID_PREFIXES.directory, ['organization_id']), + directoryUsers: store.collection('workos.directory_users', ID_PREFIXES.directory_user, [ 'directory_id', 'organization_id', ]), - directoryGroups: store.collection('workos.directory_groups', 'directory_grp', [ + directoryGroups: store.collection('workos.directory_groups', ID_PREFIXES.directory_group, [ 'directory_id', 'organization_id', ]), - auditLogActions: store.collection('workos.audit_log_actions', 'audit_action', ['name']), - auditLogEvents: store.collection('workos.audit_log_events', 'audit_event', [ - 'organization_id', - ]), - auditLogExports: store.collection('workos.audit_log_exports', 'audit_export', [ + auditLogActions: store.collection( + 'workos.audit_log_actions', + ID_PREFIXES.audit_log_action, + ['name'], + ), + auditLogEvents: store.collection('workos.audit_log_events', ID_PREFIXES.audit_log_event, [ 'organization_id', ]), - featureFlags: store.collection('workos.feature_flags', 'ff', ['slug']), - flagTargets: store.collection('workos.flag_targets', 'ff_target', ['flag_slug', 'resource_id']), - connectApplications: store.collection('workos.connect_applications', 'connect_app', [ - 'client_id', - ]), - clientSecrets: store.collection('workos.client_secrets', 'client_secret', ['application_id']), - dataIntegrationAuths: store.collection('workos.data_integration_auths', 'di_auth', [ - 'code', - 'slug', - ]), - radarAttempts: store.collection('workos.radar_attempts', 'radar_attempt', ['ip_address']), - apiKeyRecords: store.collection('workos.api_keys', 'api_key', ['key', 'environment']), - events: store.collection('workos.events', 'evt', ['event']), - webhookEndpoints: store.collection('workos.webhook_endpoints', 'we', ['url']), + auditLogExports: store.collection( + 'workos.audit_log_exports', + ID_PREFIXES.audit_log_export, + ['organization_id'], + ), + featureFlags: store.collection('workos.feature_flags', ID_PREFIXES.feature_flag, ['slug']), + flagTargets: store.collection('workos.flag_targets', ID_PREFIXES.flag_target, [ + 'flag_slug', + 'resource_id', + ]), + connectApplications: store.collection( + 'workos.connect_applications', + ID_PREFIXES.connect_application, + ['client_id'], + ), + clientSecrets: store.collection( + 'workos.client_secrets', + ID_PREFIXES.client_secret, + ['application_id'], + ), + dataIntegrationAuths: store.collection( + 'workos.data_integration_auths', + ID_PREFIXES.data_integration_auth, + ['code', 'slug'], + ), + radarAttempts: store.collection('workos.radar_attempts', ID_PREFIXES.radar_attempt, [ + 'ip_address', + ]), + apiKeyRecords: store.collection('workos.api_keys', ID_PREFIXES.api_key, ['key', 'environment']), + events: store.collection('workos.events', ID_PREFIXES.event, ['event']), + webhookEndpoints: store.collection( + 'workos.webhook_endpoints', + ID_PREFIXES.webhook_endpoint, + ['url'], + ), }; - store.setData(CACHE_KEY, ws); + store.setData(STORE_KEYS.workosStore, ws); return ws; } From f8c7d9e1ebe947fa24d6c058eaaebef7e91b8a45 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 26 Mar 2026 15:24:47 -0500 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20emulator=20simplification=20pha?= =?UTF-8?q?se=202=20=E2=80=94=20domain=20helpers=20and=20route=20dedup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract role lookup helpers (findEnvRole, findOrgRole, requireEnvRole, requireOrgRole) and role-permission CRUD helpers (getRolePermissions, replaceRolePermissions) into role-helpers.ts. Create registerRoleRoutes() factory that deduplicates ~120 lines of shared CRUD + permissions logic between authorization-roles.ts and authorization-org-roles.ts. Both files are now thin wrappers. Adopt Collection.deleteBy() at all 9 cascade delete sites across 5 files. Add formatFlagTarget() formatter and extract evaluateFlags() to deduplicate identical flag evaluation logic for orgs and users. Review: cycle 1/3, PASS. 0 critical, 0 high findings. --- src/emulate/workos/helpers.ts | 5 + src/emulate/workos/role-helpers.ts | 167 +++++++++++++++ .../workos/routes/authorization-org-roles.ts | 190 ++---------------- .../routes/authorization-permissions.ts | 5 +- .../workos/routes/authorization-roles.ts | 143 +------------ src/emulate/workos/routes/directories.ts | 7 +- src/emulate/workos/routes/feature-flags.ts | 72 ++----- src/emulate/workos/routes/organizations.ts | 11 +- 8 files changed, 229 insertions(+), 371 deletions(-) create mode 100644 src/emulate/workos/role-helpers.ts diff --git a/src/emulate/workos/helpers.ts b/src/emulate/workos/helpers.ts index 3a42ebc..194aea3 100644 --- a/src/emulate/workos/helpers.ts +++ b/src/emulate/workos/helpers.ts @@ -33,6 +33,7 @@ import type { WorkOSAuditLogEvent, WorkOSAuditLogExport, WorkOSFeatureFlag, + WorkOSFlagTarget, WorkOSConnectApplication, WorkOSClientSecret, WorkOSRadarAttempt, @@ -261,6 +262,10 @@ export function formatFeatureFlag(f: WorkOSFeatureFlag): Record return formatEntity(f); } +export function formatFlagTarget(t: WorkOSFlagTarget): Record { + return formatEntity(t); +} + export function formatConnectApplication(a: WorkOSConnectApplication): Record { return formatEntity(a); } diff --git a/src/emulate/workos/role-helpers.ts b/src/emulate/workos/role-helpers.ts new file mode 100644 index 0000000..086aea8 --- /dev/null +++ b/src/emulate/workos/role-helpers.ts @@ -0,0 +1,167 @@ +import type { Context } from 'hono'; +import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../core/index.js'; +import type { WorkOSStore } from './store.js'; +import type { WorkOSRole, WorkOSPermission } from './entities.js'; +import { getWorkOSStore } from './store.js'; +import { formatRole, formatPermission, formatListResponse } from './helpers.js'; + +export function findEnvRole(ws: WorkOSStore, slug: string): WorkOSRole | undefined { + return ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); +} + +export function findOrgRole(ws: WorkOSStore, orgId: string, slug: string): WorkOSRole | undefined { + return ws.roles.findBy('organization_id', orgId).find((r) => r.slug === slug && r.type === 'OrganizationRole'); +} + +export function requireEnvRole(ws: WorkOSStore, slug: string): WorkOSRole { + const role = findEnvRole(ws, slug); + if (!role) throw notFound('Role'); + return role; +} + +export function requireOrgRole(ws: WorkOSStore, orgId: string, slug: string): WorkOSRole { + const role = findOrgRole(ws, orgId, slug); + if (!role) throw notFound('Role'); + return role; +} + +export function getRolePermissions(ws: WorkOSStore, roleId: string): WorkOSPermission[] { + const rps = ws.rolePermissions.findBy('role_id', roleId); + return rps.map((rp) => ws.permissions.get(rp.permission_id)).filter(Boolean) as WorkOSPermission[]; +} + +export function replaceRolePermissions(ws: WorkOSStore, roleId: string, permissionSlugs: string[]): WorkOSPermission[] { + // Delete existing + ws.rolePermissions.deleteBy('role_id', roleId); + + // Insert new + for (const permSlug of permissionSlugs) { + const perm = ws.permissions.findOneBy('slug', permSlug); + if (!perm) throw notFound('Permission'); + ws.rolePermissions.insert({ role_id: roleId, permission_id: perm.id }); + } + + return getRolePermissions(ws, roleId); +} + +export interface RoleRouteConfig { + pathPrefix: string; + roleType: 'EnvironmentRole' | 'OrganizationRole'; + requireRole: (ws: WorkOSStore, c: Context) => WorkOSRole; + findRole: (ws: WorkOSStore, c: Context, slug: string) => WorkOSRole | undefined; + listFilter: (c: Context) => (r: WorkOSRole) => boolean; + insertDefaults: (c: Context) => Partial; + duplicateMessage: string; + validateBeforeCreate?: (ws: WorkOSStore, c: Context) => void; +} + +export function registerRoleRoutes(ctx: RouteContext, config: RoleRouteConfig): void { + const { app, store } = ctx; + const ws = getWorkOSStore(store); + const { pathPrefix } = config; + + app.post(pathPrefix, async (c) => { + config.validateBeforeCreate?.(ws, c); + + const body = await parseJsonBody(c); + const slug = body.slug as string; + const name = body.name as string; + + if (!slug || typeof slug !== 'string') { + throw validationError('slug is required', [{ field: 'slug', code: 'required' }]); + } + if (!name || typeof name !== 'string') { + throw validationError('name is required', [{ field: 'name', code: 'required' }]); + } + + const existing = config.findRole(ws, c, slug); + if (existing) { + throw validationError(config.duplicateMessage, [{ field: 'slug', code: 'duplicate' }]); + } + + const defaults = config.insertDefaults(c); + const role = ws.roles.insert({ + object: 'role', + slug, + name, + description: (body.description as string) ?? null, + type: config.roleType, + organization_id: defaults.organization_id ?? null, + is_default_role: Boolean(body.is_default_role), + priority: typeof body.priority === 'number' ? body.priority : 0, + }); + + return c.json(formatRole(role), 201); + }); + + app.get(pathPrefix, (c) => { + const url = new URL(c.req.url); + const params = parseListParams(url); + + const result = ws.roles.list({ + ...params, + filter: config.listFilter(c), + }); + + return c.json(formatListResponse(result, formatRole)); + }); + + app.get(`${pathPrefix}/:slug`, (c) => { + const role = config.requireRole(ws, c); + return c.json(formatRole(role)); + }); + + app.put(`${pathPrefix}/:slug`, async (c) => { + const role = config.requireRole(ws, c); + + const body = await parseJsonBody(c); + const updates: Record = {}; + if ('name' in body) updates.name = body.name; + if ('description' in body) updates.description = body.description ?? null; + if ('is_default_role' in body) updates.is_default_role = Boolean(body.is_default_role); + if ('priority' in body) updates.priority = body.priority; + + const updated = ws.roles.update(role.id, updates); + return c.json(formatRole(updated!)); + }); + + app.delete(`${pathPrefix}/:slug`, (c) => { + const role = config.requireRole(ws, c); + + ws.rolePermissions.deleteBy('role_id', role.id); + ws.roleAssignments.deleteBy('role_id', role.id); + + ws.roles.delete(role.id); + return c.body(null, 204); + }); + + // Role permissions management + app.get(`${pathPrefix}/:slug/permissions`, (c) => { + const role = config.requireRole(ws, c); + const permissions = getRolePermissions(ws, role.id); + + return c.json({ + object: 'list', + data: permissions.map((p) => formatPermission(p)), + list_metadata: { before: null, after: null }, + }); + }); + + app.post(`${pathPrefix}/:slug/permissions`, async (c) => { + const role = config.requireRole(ws, c); + + const body = await parseJsonBody(c); + const permissionSlugs = body.permissions as string[]; + if (!Array.isArray(permissionSlugs)) { + throw validationError('permissions must be an array of slugs', [{ field: 'permissions', code: 'invalid' }]); + } + + const permissions = replaceRolePermissions(ws, role.id, permissionSlugs); + + return c.json({ + object: 'list', + data: permissions.map((p) => formatPermission(p)), + list_metadata: { before: null, after: null }, + }); + }); +} diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index 7bf2c47..0dbe1f1 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -1,66 +1,15 @@ -import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; +import { type RouteContext, notFound, validationError, parseJsonBody } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; -import { formatRole, formatPermission, formatListResponse } from '../helpers.js'; +import { formatRole } from '../helpers.js'; +import { findOrgRole, requireOrgRole, registerRoleRoutes } from '../role-helpers.js'; export function authorizationOrgRoleRoutes(ctx: RouteContext): void { const { app, store } = ctx; const ws = getWorkOSStore(store); - - app.post('/authorization/organizations/:orgId/roles', async (c) => { - const orgId = c.req.param('orgId'); - const org = ws.organizations.get(orgId); - if (!org) throw notFound('Organization'); - - const body = await parseJsonBody(c); - const slug = body.slug as string; - const name = body.name as string; - - if (!slug || typeof slug !== 'string') { - throw validationError('slug is required', [{ field: 'slug', code: 'required' }]); - } - if (!name || typeof name !== 'string') { - throw validationError('name is required', [{ field: 'name', code: 'required' }]); - } - - // Check uniqueness within this org - const existing = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (existing) { - throw validationError('Role with this slug already exists in this organization', [ - { field: 'slug', code: 'duplicate' }, - ]); - } - - const role = ws.roles.insert({ - object: 'role', - slug, - name, - description: (body.description as string) ?? null, - type: 'OrganizationRole', - organization_id: orgId, - is_default_role: Boolean(body.is_default_role), - priority: typeof body.priority === 'number' ? body.priority : 0, - }); - - return c.json(formatRole(role), 201); - }); - - app.get('/authorization/organizations/:orgId/roles', (c) => { - const orgId = c.req.param('orgId'); - const url = new URL(c.req.url); - const params = parseListParams(url); - - const result = ws.roles.list({ - ...params, - filter: (r) => r.organization_id === orgId && r.type === 'OrganizationRole', - }); - - return c.json(formatListResponse(result, formatRole)); - }); + const prefix = '/authorization/organizations/:orgId/roles'; // Priority ordering — must be registered before :slug routes - app.put('/authorization/organizations/:orgId/roles/priority', async (c) => { + app.put(`${prefix}/priority`, async (c) => { const orgId = c.req.param('orgId'); const body = await parseJsonBody(c); const slugs = body.slugs as string[]; @@ -70,10 +19,7 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { } for (let i = 0; i < slugs.length; i++) { - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slugs[i] && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); + const role = requireOrgRole(ws, orgId, slugs[i]!); ws.roles.update(role.id, { priority: i }); } @@ -89,117 +35,25 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { }); }); - app.get('/authorization/organizations/:orgId/roles/:slug', (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); - return c.json(formatRole(role)); - }); - - app.put('/authorization/organizations/:orgId/roles/:slug', async (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); - - const body = await parseJsonBody(c); - const updates: Record = {}; - if ('name' in body) updates.name = body.name; - if ('description' in body) updates.description = body.description ?? null; - if ('is_default_role' in body) updates.is_default_role = Boolean(body.is_default_role); - if ('priority' in body) updates.priority = body.priority; - - const updated = ws.roles.update(role.id, updates); - return c.json(formatRole(updated!)); + registerRoleRoutes(ctx, { + pathPrefix: prefix, + roleType: 'OrganizationRole', + requireRole: (ws, c) => requireOrgRole(ws, c.req.param('orgId'), c.req.param('slug')), + findRole: (ws, c, slug) => findOrgRole(ws, c.req.param('orgId'), slug), + listFilter: (c) => (r) => r.organization_id === c.req.param('orgId') && r.type === 'OrganizationRole', + insertDefaults: (c) => ({ organization_id: c.req.param('orgId') }), + duplicateMessage: 'Role with this slug already exists in this organization', + validateBeforeCreate: (ws, c) => { + const org = ws.organizations.get(c.req.param('orgId')); + if (!org) throw notFound('Organization'); + }, }); - app.delete('/authorization/organizations/:orgId/roles/:slug', (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); - - // Cascade: remove role-permission joins and role assignments - const rps = ws.rolePermissions.findBy('role_id', role.id); - for (const rp of rps) ws.rolePermissions.delete(rp.id); - const ras = ws.roleAssignments.findBy('role_id', role.id); - for (const ra of ras) ws.roleAssignments.delete(ra.id); - - ws.roles.delete(role.id); - return c.body(null, 204); - }); - - // Org role permissions - app.get('/authorization/organizations/:orgId/roles/:slug/permissions', (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); - - const rps = ws.rolePermissions.findBy('role_id', role.id); - const permissions = rps.map((rp) => ws.permissions.get(rp.permission_id)).filter(Boolean); - - return c.json({ - object: 'list', - data: permissions.map((p) => formatPermission(p!)), - list_metadata: { before: null, after: null }, - }); - }); - - app.post('/authorization/organizations/:orgId/roles/:slug/permissions', async (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); - - const body = await parseJsonBody(c); - const permissionSlugs = body.permissions as string[]; - if (!Array.isArray(permissionSlugs)) { - throw validationError('permissions must be an array of slugs', [{ field: 'permissions', code: 'invalid' }]); - } - - // Replace all - const existing = ws.rolePermissions.findBy('role_id', role.id); - for (const rp of existing) ws.rolePermissions.delete(rp.id); - - for (const permSlug of permissionSlugs) { - const perm = ws.permissions.findOneBy('slug', permSlug); - if (!perm) throw notFound('Permission'); - ws.rolePermissions.insert({ role_id: role.id, permission_id: perm.id }); - } - - const rps = ws.rolePermissions.findBy('role_id', role.id); - const permissions = rps.map((rp) => ws.permissions.get(rp.permission_id)).filter(Boolean); - - return c.json({ - object: 'list', - data: permissions.map((p) => formatPermission(p!)), - list_metadata: { before: null, after: null }, - }); - }); - - app.delete('/authorization/organizations/:orgId/roles/:slug/permissions/:permissionSlug', (c) => { - const orgId = c.req.param('orgId'); - const slug = c.req.param('slug'); - const permissionSlug = c.req.param('permissionSlug'); - - const role = ws.roles - .findBy('organization_id', orgId) - .find((r) => r.slug === slug && r.type === 'OrganizationRole'); - if (!role) throw notFound('Role'); + // Org-specific: delete single permission from role + app.delete(`${prefix}/:slug/permissions/:permissionSlug`, (c) => { + const role = requireOrgRole(ws, c.req.param('orgId'), c.req.param('slug')); - const perm = ws.permissions.findOneBy('slug', permissionSlug); + const perm = ws.permissions.findOneBy('slug', c.req.param('permissionSlug')); if (!perm) throw notFound('Permission'); const rp = ws.rolePermissions.findBy('role_id', role.id).find((rp) => rp.permission_id === perm.id); diff --git a/src/emulate/workos/routes/authorization-permissions.ts b/src/emulate/workos/routes/authorization-permissions.ts index 8fadef4..1bfcc97 100644 --- a/src/emulate/workos/routes/authorization-permissions.ts +++ b/src/emulate/workos/routes/authorization-permissions.ts @@ -68,10 +68,7 @@ export function authorizationPermissionRoutes(ctx: RouteContext): void { if (!permission) throw notFound('Permission'); // Cascade: remove from all role-permission joins - const rps = ws.rolePermissions.findBy('permission_id', permission.id); - for (const rp of rps) { - ws.rolePermissions.delete(rp.id); - } + ws.rolePermissions.deleteBy('permission_id', permission.id); ws.permissions.delete(permission.id); return c.body(null, 204); diff --git a/src/emulate/workos/routes/authorization-roles.ts b/src/emulate/workos/routes/authorization-roles.ts index ec2128f..3db6f85 100644 --- a/src/emulate/workos/routes/authorization-roles.ts +++ b/src/emulate/workos/routes/authorization-roles.ts @@ -1,137 +1,14 @@ -import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; -import { getWorkOSStore } from '../store.js'; -import { formatRole, formatPermission, formatListResponse } from '../helpers.js'; +import type { RouteContext } from '../../core/index.js'; +import { findEnvRole, requireEnvRole, registerRoleRoutes } from '../role-helpers.js'; export function authorizationRoleRoutes(ctx: RouteContext): void { - const { app, store } = ctx; - const ws = getWorkOSStore(store); - - app.post('/authorization/roles', async (c) => { - const body = await parseJsonBody(c); - const slug = body.slug as string; - const name = body.name as string; - - if (!slug || typeof slug !== 'string') { - throw validationError('slug is required', [{ field: 'slug', code: 'required' }]); - } - if (!name || typeof name !== 'string') { - throw validationError('name is required', [{ field: 'name', code: 'required' }]); - } - - // Check uniqueness among environment roles - const existing = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (existing) { - throw validationError('Role with this slug already exists', [{ field: 'slug', code: 'duplicate' }]); - } - - const role = ws.roles.insert({ - object: 'role', - slug, - name, - description: (body.description as string) ?? null, - type: 'EnvironmentRole', - organization_id: null, - is_default_role: Boolean(body.is_default_role), - priority: typeof body.priority === 'number' ? body.priority : 0, - }); - - return c.json(formatRole(role), 201); - }); - - app.get('/authorization/roles', (c) => { - const url = new URL(c.req.url); - const params = parseListParams(url); - - const result = ws.roles.list({ - ...params, - filter: (r) => r.type === 'EnvironmentRole', - }); - - return c.json(formatListResponse(result, formatRole)); - }); - - app.get('/authorization/roles/:slug', (c) => { - const slug = c.req.param('slug'); - const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (!role) throw notFound('Role'); - return c.json(formatRole(role)); - }); - - app.put('/authorization/roles/:slug', async (c) => { - const slug = c.req.param('slug'); - const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (!role) throw notFound('Role'); - - const body = await parseJsonBody(c); - const updates: Record = {}; - if ('name' in body) updates.name = body.name; - if ('description' in body) updates.description = body.description ?? null; - if ('is_default_role' in body) updates.is_default_role = Boolean(body.is_default_role); - if ('priority' in body) updates.priority = body.priority; - - const updated = ws.roles.update(role.id, updates); - return c.json(formatRole(updated!)); - }); - - app.delete('/authorization/roles/:slug', (c) => { - const slug = c.req.param('slug'); - const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (!role) throw notFound('Role'); - - // Cascade: remove role-permission joins and role assignments - const rps = ws.rolePermissions.findBy('role_id', role.id); - for (const rp of rps) ws.rolePermissions.delete(rp.id); - const ras = ws.roleAssignments.findBy('role_id', role.id); - for (const ra of ras) ws.roleAssignments.delete(ra.id); - - ws.roles.delete(role.id); - return c.body(null, 204); - }); - - // Role permissions management - app.get('/authorization/roles/:slug/permissions', (c) => { - const slug = c.req.param('slug'); - const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (!role) throw notFound('Role'); - - const rps = ws.rolePermissions.findBy('role_id', role.id); - const permissions = rps.map((rp) => ws.permissions.get(rp.permission_id)).filter(Boolean); - - return c.json({ - object: 'list', - data: permissions.map((p) => formatPermission(p!)), - list_metadata: { before: null, after: null }, - }); - }); - - app.post('/authorization/roles/:slug/permissions', async (c) => { - const slug = c.req.param('slug'); - const role = ws.roles.findBy('slug', slug).find((r) => r.type === 'EnvironmentRole'); - if (!role) throw notFound('Role'); - - const body = await parseJsonBody(c); - const permissionSlugs = body.permissions as string[]; - if (!Array.isArray(permissionSlugs)) { - throw validationError('permissions must be an array of slugs', [{ field: 'permissions', code: 'invalid' }]); - } - - // Replace all: delete existing, add new - const existing = ws.rolePermissions.findBy('role_id', role.id); - for (const rp of existing) ws.rolePermissions.delete(rp.id); - - for (const permSlug of permissionSlugs) { - const perm = ws.permissions.findOneBy('slug', permSlug); - if (!perm) throw notFound('Permission'); - ws.rolePermissions.insert({ role_id: role.id, permission_id: perm.id }); - } - - const rps = ws.rolePermissions.findBy('role_id', role.id); - const permissions = rps.map((rp) => ws.permissions.get(rp.permission_id)).filter(Boolean); - - return c.json({ - object: 'list', - data: permissions.map((p) => formatPermission(p!)), - list_metadata: { before: null, after: null }, - }); + registerRoleRoutes(ctx, { + pathPrefix: '/authorization/roles', + roleType: 'EnvironmentRole', + requireRole: (ws, c) => requireEnvRole(ws, c.req.param('slug')), + findRole: (ws, _c, slug) => findEnvRole(ws, slug), + listFilter: () => (r) => r.type === 'EnvironmentRole', + insertDefaults: () => ({ organization_id: null }), + duplicateMessage: 'Role with this slug already exists', }); } diff --git a/src/emulate/workos/routes/directories.ts b/src/emulate/workos/routes/directories.ts index 46e08e0..ad6257a 100644 --- a/src/emulate/workos/routes/directories.ts +++ b/src/emulate/workos/routes/directories.ts @@ -37,11 +37,8 @@ export function directoryRoutes(ctx: RouteContext): void { const dir = ws.directories.get(c.req.param('id')); if (!dir) throw notFound('Directory'); - const users = ws.directoryUsers.findBy('directory_id', dir.id); - for (const u of users) ws.directoryUsers.delete(u.id); - - const groups = ws.directoryGroups.findBy('directory_id', dir.id); - for (const g of groups) ws.directoryGroups.delete(g.id); + ws.directoryUsers.deleteBy('directory_id', dir.id); + ws.directoryGroups.deleteBy('directory_id', dir.id); ws.directories.delete(dir.id); return c.body(null, 204); diff --git a/src/emulate/workos/routes/feature-flags.ts b/src/emulate/workos/routes/feature-flags.ts index f51c99d..36c9fb6 100644 --- a/src/emulate/workos/routes/feature-flags.ts +++ b/src/emulate/workos/routes/feature-flags.ts @@ -1,6 +1,19 @@ import { type RouteContext, notFound, parseJsonBody, parseListParams } from '../../core/index.js'; -import { getWorkOSStore } from '../store.js'; -import { formatFeatureFlag, formatListResponse } from '../helpers.js'; +import { getWorkOSStore, type WorkOSStore } from '../store.js'; +import { formatFeatureFlag, formatFlagTarget, formatListResponse } from '../helpers.js'; + +function evaluateFlags(ws: WorkOSStore, resourceId: string) { + const flags = ws.featureFlags.all(); + return flags.map((flag) => { + const target = ws.flagTargets.findBy('flag_slug', flag.slug).find((t) => t.resource_id === resourceId); + return { + slug: flag.slug, + type: flag.type, + value: target ? target.value : flag.enabled ? flag.default_value : null, + enabled: flag.enabled, + }; + }); +} export function featureFlagRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -53,14 +66,7 @@ export function featureFlagRoutes(ctx: RouteContext): void { value: body.value, resource_type: (body.resource_type as string) ?? existing.resource_type, }); - return c.json({ - object: 'flag_target', - id: updated!.id, - flag_slug: updated!.flag_slug, - resource_id: updated!.resource_id, - resource_type: updated!.resource_type, - value: updated!.value, - }); + return c.json(formatFlagTarget(updated!)); } const target = ws.flagTargets.insert({ @@ -71,17 +77,7 @@ export function featureFlagRoutes(ctx: RouteContext): void { value: body.value, }); - return c.json( - { - object: 'flag_target', - id: target.id, - flag_slug: target.flag_slug, - resource_id: target.resource_id, - resource_type: target.resource_type, - value: target.value, - }, - 201, - ); + return c.json(formatFlagTarget(target), 201); }); // Remove target @@ -99,46 +95,18 @@ export function featureFlagRoutes(ctx: RouteContext): void { // Evaluate flags for organization app.get('/organizations/:orgId/feature-flags', (c) => { - const orgId = c.req.param('orgId'); - const flags = ws.featureFlags.all(); - - const evaluations = flags.map((flag) => { - const target = ws.flagTargets.findBy('flag_slug', flag.slug).find((t) => t.resource_id === orgId); - - return { - slug: flag.slug, - type: flag.type, - value: target ? target.value : flag.enabled ? flag.default_value : null, - enabled: flag.enabled, - }; - }); - return c.json({ object: 'list', - data: evaluations, + data: evaluateFlags(ws, c.req.param('orgId')), list_metadata: { before: null, after: null }, }); }); - // Evaluate flags for user (replaces stub in user-features.ts) + // Evaluate flags for user app.get('/user_management/users/:userId/feature-flags', (c) => { - const userId = c.req.param('userId'); - const flags = ws.featureFlags.all(); - - const evaluations = flags.map((flag) => { - const target = ws.flagTargets.findBy('flag_slug', flag.slug).find((t) => t.resource_id === userId); - - return { - slug: flag.slug, - type: flag.type, - value: target ? target.value : flag.enabled ? flag.default_value : null, - enabled: flag.enabled, - }; - }); - return c.json({ object: 'list', - data: evaluations, + data: evaluateFlags(ws, c.req.param('userId')), list_metadata: { before: null, after: null }, }); }); diff --git a/src/emulate/workos/routes/organizations.ts b/src/emulate/workos/routes/organizations.ts index 172edb3..1c28b1a 100644 --- a/src/emulate/workos/routes/organizations.ts +++ b/src/emulate/workos/routes/organizations.ts @@ -127,15 +127,8 @@ export function organizationRoutes(ctx: RouteContext): void { const org = ws.organizations.get(c.req.param('id')); if (!org) throw notFound('Organization'); - const domains = ws.organizationDomains.findBy('organization_id', org.id); - for (const d of domains) { - ws.organizationDomains.delete(d.id); - } - - const memberships = ws.organizationMemberships.findBy('organization_id', org.id); - for (const m of memberships) { - ws.organizationMemberships.delete(m.id); - } + ws.organizationDomains.deleteBy('organization_id', org.id); + ws.organizationMemberships.deleteBy('organization_id', org.id); ws.organizations.delete(org.id); return c.body(null, 204); From 330f3f2972a204048a61738a62bf749df3e96a97 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 26 Mar 2026 18:22:06 -0500 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20emulator=20simplification=20pha?= =?UTF-8?q?se=203=20=E2=80=94=20server=20infrastructure=20and=20efficiency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace ~35 individual auth middleware registrations with single catch-all using PUBLIC_PATHS set and PUBLIC_PATH_PREFIXES array - Adopt unauthorized() helper in auth middleware (removes 3 inline 401s) - Remove defensive array spread in cursorPaginate when no filter applied - Add optional pre-fetched domains to formatOrganization, batch in list endpoint - Add event-type index to EventBus with rebuildIndex() for pre-filtering - Fix N+1 in priority reorder endpoint with slug-to-role Map - Add Store.deleteDataByPrefix() for temporal store data cleanup --- src/emulate/core/middleware/auth.ts | 32 ++-------- src/emulate/core/pagination.ts | 3 +- src/emulate/core/server.ts | 60 ++++++------------- src/emulate/core/store.ts | 11 ++++ src/emulate/workos/event-bus.spec.ts | 4 ++ src/emulate/workos/event-bus.ts | 38 ++++++++++-- src/emulate/workos/helpers.ts | 8 ++- src/emulate/workos/index.ts | 5 ++ .../workos/routes/authorization-org-roles.ts | 14 ++++- src/emulate/workos/routes/organizations.ts | 12 +++- 10 files changed, 105 insertions(+), 82 deletions(-) diff --git a/src/emulate/core/middleware/auth.ts b/src/emulate/core/middleware/auth.ts index 8fcfd72..7c52644 100644 --- a/src/emulate/core/middleware/auth.ts +++ b/src/emulate/core/middleware/auth.ts @@ -1,4 +1,5 @@ import type { Context, Next } from 'hono'; +import { unauthorized } from './error-handler.js'; export interface WorkOSAuthContext { environment: string; @@ -17,38 +18,13 @@ export type ApiKeyMap = Record; export function authMiddleware(apiKeys: ApiKeyMap) { return async (c: Context, next: Next) => { const authHeader = c.req.header('Authorization'); - if (!authHeader) { - return c.json( - { - message: 'Unauthorized', - code: 'unauthorized', - }, - 401, - ); - } + if (!authHeader) throw unauthorized(); const token = authHeader.replace(/^Bearer\s+/i, '').trim(); - - if (!token.startsWith('sk_')) { - return c.json( - { - message: 'Unauthorized', - code: 'unauthorized', - }, - 401, - ); - } + if (!token.startsWith('sk_')) throw unauthorized(); const keyInfo = apiKeys[token]; - if (!keyInfo) { - return c.json( - { - message: 'Unauthorized', - code: 'unauthorized', - }, - 401, - ); - } + if (!keyInfo) throw unauthorized(); c.set('auth', { environment: keyInfo.environment, apiKey: token } satisfies WorkOSAuthContext); await next(); diff --git a/src/emulate/core/pagination.ts b/src/emulate/core/pagination.ts index 8010a7a..f0bb8e4 100644 --- a/src/emulate/core/pagination.ts +++ b/src/emulate/core/pagination.ts @@ -33,7 +33,8 @@ export function cursorPaginate( items: T[], options: CursorPaginationOptions = {}, ): CursorPaginatedResult { - let filtered = options.filter ? items.filter(options.filter) : [...items]; + // Callers must pass a fresh array (e.g. Collection.all()) — sort mutates in-place + let filtered = options.filter ? items.filter(options.filter) : items; const order = options.order ?? 'desc'; const defaultSort = (a: T, b: T) => diff --git a/src/emulate/core/server.ts b/src/emulate/core/server.ts index 6b60eb7..d484879 100644 --- a/src/emulate/core/server.ts +++ b/src/emulate/core/server.ts @@ -33,59 +33,37 @@ export function createServer(plugin: ServicePlugin, options: ServerOptions = {}) return c.json(jwt.getJWKS()); }); - // Auth middleware — single instance, shared across all routes + // Auth middleware — single catch-all instance const auth = authMiddleware(apiKeys); const PUBLIC_PATHS = new Set([ + '/health', '/user_management/authorize', '/user_management/authenticate', '/user_management/sessions/logout', ]); - app.use('/api/*', auth); - app.use('/user_management/*', async (c, next) => { + const PUBLIC_PATH_PREFIXES = [ + '/sso/', + '/user_management/sessions/jwks/', + '/data-integrations/', + ]; + + app.use('*', async (c, next) => { const path = new URL(c.req.url).pathname; - if (PUBLIC_PATHS.has(path) || path.startsWith('/user_management/sessions/jwks/')) { - return next(); + + // Skip auth for public paths + if (PUBLIC_PATHS.has(path)) return next(); + for (const prefix of PUBLIC_PATH_PREFIXES) { + if (path.startsWith(prefix)) { + // data-integrations: only /authorize subpath is public + if (prefix === '/data-integrations/' && !path.endsWith('/authorize')) break; + return next(); + } } + return auth(c, next); }); - app.use('/x/authkit/*', auth); - app.use('/organizations', auth); - app.use('/organizations/*', auth); - app.use('/organization_memberships', auth); - app.use('/organization_memberships/*', auth); - app.use('/organization_domains', auth); - app.use('/organization_domains/*', auth); - app.use('/connections', auth); - app.use('/connections/*', auth); - app.use('/directories', auth); - app.use('/directories/*', auth); - app.use('/directory_groups', auth); - app.use('/directory_groups/*', auth); - app.use('/directory_users', auth); - app.use('/directory_users/*', auth); - app.use('/events', auth); - app.use('/events/*', auth); - app.use('/pipes/*', auth); - app.use('/audit_logs/*', auth); - app.use('/feature-flags', auth); - app.use('/feature-flags/*', auth); - app.use('/connect/*', auth); - app.use('/data-integrations/*', async (c, next) => { - const path = new URL(c.req.url).pathname; - if (path.endsWith('/authorize')) return next(); - return auth(c, next); - }); - app.use('/radar/*', auth); - app.use('/api_keys', auth); - app.use('/api_keys/*', auth); - app.use('/portal/*', auth); - app.use('/webhook_endpoints', auth); - app.use('/webhook_endpoints/*', auth); - app.use('/auth/factors', auth); - app.use('/auth/factors/*', auth); - app.use('/auth/challenges/*', auth); // Rate limiting const rateLimitCounters = new Map(); diff --git a/src/emulate/core/store.ts b/src/emulate/core/store.ts index d0c6f5a..a899698 100644 --- a/src/emulate/core/store.ts +++ b/src/emulate/core/store.ts @@ -178,6 +178,17 @@ export class Store { this._data.set(key, value); } + deleteDataByPrefix(prefix: string): number { + let count = 0; + for (const key of this._data.keys()) { + if (key.startsWith(prefix)) { + this._data.delete(key); + count++; + } + } + return count; + } + reset(): void { for (const collection of this.collections.values()) { collection.clear(); diff --git a/src/emulate/workos/event-bus.spec.ts b/src/emulate/workos/event-bus.spec.ts index daf5a3d..591c6bf 100644 --- a/src/emulate/workos/event-bus.spec.ts +++ b/src/emulate/workos/event-bus.spec.ts @@ -54,6 +54,7 @@ describe('EventBus', () => { description: null, }); + bus.rebuildIndex(); // This should not attempt delivery (no fetch error even though URL is unreachable) bus.emit({ event: 'user.created', data: {} }); expect(ws.events.all()).toHaveLength(1); @@ -70,6 +71,7 @@ describe('EventBus', () => { description: null, }); + bus.rebuildIndex(); // user.created should not match the endpoint's filter bus.emit({ event: 'user.created', data: {} }); expect(ws.events.all()).toHaveLength(1); @@ -93,6 +95,7 @@ describe('EventBus', () => { description: null, }); + bus.rebuildIndex(); bus.emit({ event: 'user.created', data: { id: 'user_1' } }); // Wait for async delivery @@ -132,6 +135,7 @@ describe('EventBus', () => { description: null, }); + bus.rebuildIndex(); // emit() should return immediately (fire-and-forget) const start = Date.now(); bus.emit({ event: 'user.created', data: {} }); diff --git a/src/emulate/workos/event-bus.ts b/src/emulate/workos/event-bus.ts index 413c3d0..a089290 100644 --- a/src/emulate/workos/event-bus.ts +++ b/src/emulate/workos/event-bus.ts @@ -11,8 +11,30 @@ export interface EventPayload { } export class EventBus { + private endpointsByEvent = new Map>(); + private catchAllEndpoints = new Set(); + constructor(private store: Store) {} + /** Rebuild the event-type index. Call after webhook endpoint CRUD or seed. */ + rebuildIndex(): void { + this.endpointsByEvent.clear(); + this.catchAllEndpoints.clear(); + const ws = getWorkOSStore(this.store); + for (const ep of ws.webhookEndpoints.all()) { + if (!ep.enabled) continue; + if (ep.events.length === 0) { + this.catchAllEndpoints.add(ep.id); + } else { + for (const evt of ep.events) { + const set = this.endpointsByEvent.get(evt) ?? new Set(); + set.add(ep.id); + this.endpointsByEvent.set(evt, set); + } + } + } + } + emit(payload: EventPayload): void { const ws = getWorkOSStore(this.store); @@ -23,12 +45,16 @@ export class EventBus { environment_id: payload.environment_id ?? null, }); - const endpoints = ws.webhookEndpoints.all(); - for (const endpoint of endpoints) { - if (!endpoint.enabled) continue; - if (endpoint.events.length > 0 && !endpoint.events.includes(payload.event)) continue; - // Fire-and-forget — don't await - this.deliver(endpoint, event).catch(() => {}); + // Pre-filtered: only endpoints that care about this event + const targetIds = new Set(this.catchAllEndpoints); + const eventSpecific = this.endpointsByEvent.get(payload.event); + if (eventSpecific) { + for (const id of eventSpecific) targetIds.add(id); + } + + for (const id of targetIds) { + const endpoint = ws.webhookEndpoints.get(id); + if (endpoint) this.deliver(endpoint, event).catch(() => {}); } } diff --git a/src/emulate/workos/helpers.ts b/src/emulate/workos/helpers.ts index 194aea3..5edcb9c 100644 --- a/src/emulate/workos/helpers.ts +++ b/src/emulate/workos/helpers.ts @@ -67,8 +67,12 @@ export function formatListResponse( }; } -export function formatOrganization(org: WorkOSOrganization, ws: WorkOSStore): Record { - const domains = ws.organizationDomains.findBy('organization_id', org.id).map(formatDomain); +export function formatOrganization( + org: WorkOSOrganization, + ws: WorkOSStore, + opts?: { domains?: WorkOSOrganizationDomain[] }, +): Record { + const domains = (opts?.domains ?? ws.organizationDomains.findBy('organization_id', org.id)).map(formatDomain); return { object: 'organization', diff --git a/src/emulate/workos/index.ts b/src/emulate/workos/index.ts index 19090d8..fe07d0d 100644 --- a/src/emulate/workos/index.ts +++ b/src/emulate/workos/index.ts @@ -432,6 +432,11 @@ export const workosPlugin: ServicePlugin = { onUpdate: (g) => eventBus.emit({ event: EVENTS.directoryGroupUpdated, data: formatDirectoryGroup(g) }), onDelete: (g) => eventBus.emit({ event: EVENTS.directoryGroupDeleted, data: formatDirectoryGroup(g) }), }); + ws.webhookEndpoints.setHooks({ + onInsert: () => eventBus.rebuildIndex(), + onUpdate: () => eventBus.rebuildIndex(), + onDelete: () => eventBus.rebuildIndex(), + }); }, seed(_store: Store, _baseUrl: string): void { // No default seed data — users provide their own via seedFromConfig diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index 0dbe1f1..573cb4c 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -18,19 +18,27 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { throw validationError('slugs must be an array', [{ field: 'slugs', code: 'invalid' }]); } + // Fetch once, build slug map for O(1) lookups + const orgRoles = ws.roles + .findBy('organization_id', orgId) + .filter((r) => r.type === 'OrganizationRole'); + const rolesBySlug = new Map(orgRoles.map((r) => [r.slug, r])); + for (let i = 0; i < slugs.length; i++) { - const role = requireOrgRole(ws, orgId, slugs[i]!); + const role = rolesBySlug.get(slugs[i]!); + if (!role) throw notFound('Role'); ws.roles.update(role.id, { priority: i }); } - const roles = ws.roles + // Re-fetch for updated priority values + const updated = ws.roles .findBy('organization_id', orgId) .filter((r) => r.type === 'OrganizationRole') .sort((a, b) => a.priority - b.priority); return c.json({ object: 'list', - data: roles.map(formatRole), + data: updated.map(formatRole), list_metadata: { before: null, after: null }, }); }); diff --git a/src/emulate/workos/routes/organizations.ts b/src/emulate/workos/routes/organizations.ts index 1c28b1a..c768db1 100644 --- a/src/emulate/workos/routes/organizations.ts +++ b/src/emulate/workos/routes/organizations.ts @@ -1,6 +1,7 @@ import { type RouteContext, notFound, validationError, parseJsonBody, parseListParams } from '../../core/index.js'; import { getWorkOSStore } from '../store.js'; import { formatOrganization, generateVerificationToken, formatListResponse } from '../helpers.js'; +import type { WorkOSOrganizationDomain } from '../entities.js'; export function organizationRoutes(ctx: RouteContext): void { const { app, store } = ctx; @@ -61,7 +62,16 @@ export function organizationRoutes(ctx: RouteContext): void { }, }); - return c.json(formatListResponse(result, (org) => formatOrganization(org, ws))); + // Pre-fetch all domains once to avoid N+1 lookups per org + const allDomains = ws.organizationDomains.all(); + const domainsByOrg = new Map(); + for (const d of allDomains) { + const list = domainsByOrg.get(d.organization_id) ?? []; + list.push(d); + domainsByOrg.set(d.organization_id, list); + } + + return c.json(formatListResponse(result, (org) => formatOrganization(org, ws, { domains: domainsByOrg.get(org.id) ?? [] }))); }); app.get('/organizations/:id', (c) => {