Skip to content

Commit c172771

Browse files
authored
refactor: emulator simplification — shared infra, domain helpers, server efficiency (#107)
* 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 * refactor: emulator simplification phase 1 — shared infrastructure - 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) * refactor: emulator simplification phase 2 — domain helpers and route dedup 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. * refactor: emulator simplification phase 3 — server infrastructure and efficiency - 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 * chore: formatting:
1 parent 01eeb6c commit c172771

36 files changed

Lines changed: 761 additions & 1126 deletions

src/emulate/core/id.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,31 @@ export const ID_PREFIXES = {
4141
directory: 'directory',
4242
directory_user: 'directory_user',
4343
directory_group: 'directory_grp',
44-
event: 'event',
44+
event: 'evt',
4545
invitation: 'inv',
4646
session: 'session',
4747
email_verification: 'email_verification',
4848
password_reset: 'password_reset',
4949
magic_auth: 'magic_auth',
5050
authentication_factor: 'auth_factor',
5151
authentication_challenge: 'auth_challenge',
52+
authorization_code: 'auth_code',
53+
identity: 'identity',
54+
sso_authorization: 'sso_auth',
55+
refresh_token: 'ref',
56+
device_authorization: 'dev_auth',
5257
api_key: 'api_key',
5358
profile: 'prof',
5459
pipe_connection: 'pipe_conn',
60+
redirect_uri: 'redir',
61+
cors_origin: 'cors',
62+
authorized_application: 'auth_app',
63+
connected_account: 'conn_acct',
64+
role: 'role',
65+
permission: 'perm',
66+
role_permission: 'rp',
67+
authorization_resource: 'auth_res',
68+
role_assignment: 'ra',
5569
audit_log_action: 'audit_action',
5670
audit_log_event: 'audit_event',
5771
audit_log_export: 'audit_export',
@@ -61,4 +75,5 @@ export const ID_PREFIXES = {
6175
client_secret: 'client_secret',
6276
data_integration_auth: 'di_auth',
6377
radar_attempt: 'radar_attempt',
78+
webhook_endpoint: 'we',
6479
} as const;

src/emulate/core/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ export {
88
type CollectionHooks,
99
} from './store.js';
1010
export { generateId, resetIdState, ID_PREFIXES } from './id.js';
11-
export { cursorPaginate, type CursorPaginationOptions, type CursorPaginatedResult } from './pagination.js';
11+
export {
12+
parseListParams,
13+
cursorPaginate,
14+
type CursorPaginationOptions,
15+
type CursorPaginatedResult,
16+
} from './pagination.js';
1217
export { JWTManager, type JWTPayload } from './jwt.js';
1318
export { createServer, type ServerOptions } from './server.js';
1419
export { type ServicePlugin, type RouteContext } from './plugin.js';

src/emulate/core/middleware/auth.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Context, Next } from 'hono';
2+
import { unauthorized } from './error-handler.js';
23

34
export interface WorkOSAuthContext {
45
environment: string;
@@ -17,38 +18,13 @@ export type ApiKeyMap = Record<string, { environment: string }>;
1718
export function authMiddleware(apiKeys: ApiKeyMap) {
1819
return async (c: Context, next: Next) => {
1920
const authHeader = c.req.header('Authorization');
20-
if (!authHeader) {
21-
return c.json(
22-
{
23-
message: 'Unauthorized',
24-
code: 'unauthorized',
25-
},
26-
401,
27-
);
28-
}
21+
if (!authHeader) throw unauthorized();
2922

3023
const token = authHeader.replace(/^Bearer\s+/i, '').trim();
31-
32-
if (!token.startsWith('sk_')) {
33-
return c.json(
34-
{
35-
message: 'Unauthorized',
36-
code: 'unauthorized',
37-
},
38-
401,
39-
);
40-
}
24+
if (!token.startsWith('sk_')) throw unauthorized();
4125

4226
const keyInfo = apiKeys[token];
43-
if (!keyInfo) {
44-
return c.json(
45-
{
46-
message: 'Unauthorized',
47-
code: 'unauthorized',
48-
},
49-
401,
50-
);
51-
}
27+
if (!keyInfo) throw unauthorized();
5228

5329
c.set('auth', { environment: keyInfo.environment, apiKey: token } satisfies WorkOSAuthContext);
5430
await next();

src/emulate/core/pagination.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ export interface CursorPaginatedResult<T> {
2121
};
2222
}
2323

24+
export function parseListParams(url: URL) {
25+
const limit = parseInt(url.searchParams.get('limit') ?? '10') || 10;
26+
const order = (url.searchParams.get('order') as 'asc' | 'desc') ?? 'desc';
27+
const before = url.searchParams.get('before') ?? undefined;
28+
const after = url.searchParams.get('after') ?? undefined;
29+
return { limit, order, before, after };
30+
}
31+
2432
export function cursorPaginate<T extends Entity>(
2533
items: T[],
2634
options: CursorPaginationOptions<T> = {},
2735
): CursorPaginatedResult<T> {
28-
let filtered = options.filter ? items.filter(options.filter) : [...items];
36+
// Callers must pass a fresh array (e.g. Collection.all()) — sort mutates in-place
37+
let filtered = options.filter ? items.filter(options.filter) : items;
2938

3039
const order = options.order ?? 'desc';
3140
const defaultSort = (a: T, b: T) =>

src/emulate/core/server.ts

Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,57 +33,33 @@ export function createServer(plugin: ServicePlugin, options: ServerOptions = {})
3333
return c.json(jwt.getJWKS());
3434
});
3535

36-
// Auth middleware for API routes
37-
app.use('/api/*', authMiddleware(apiKeys));
38-
app.use('/user_management/*', async (c, next) => {
36+
// Auth middleware — single catch-all instance
37+
const auth = authMiddleware(apiKeys);
38+
39+
const PUBLIC_PATHS = new Set([
40+
'/health',
41+
'/user_management/authorize',
42+
'/user_management/authenticate',
43+
'/user_management/sessions/logout',
44+
]);
45+
46+
const PUBLIC_PATH_PREFIXES = ['/sso/', '/user_management/sessions/jwks/', '/data-integrations/'];
47+
48+
app.use('*', async (c, next) => {
3949
const path = new URL(c.req.url).pathname;
40-
// Public endpoints (no auth required)
41-
if (
42-
path === '/user_management/authorize' ||
43-
path === '/user_management/authenticate' ||
44-
path === '/user_management/sessions/logout' ||
45-
path.startsWith('/user_management/sessions/jwks/')
46-
) {
47-
return next();
50+
51+
// Skip auth for public paths
52+
if (PUBLIC_PATHS.has(path)) return next();
53+
for (const prefix of PUBLIC_PATH_PREFIXES) {
54+
if (path.startsWith(prefix)) {
55+
// data-integrations: only /authorize subpath is public
56+
if (prefix === '/data-integrations/' && !path.endsWith('/authorize')) break;
57+
return next();
58+
}
4859
}
49-
return authMiddleware(apiKeys)(c, next);
50-
});
51-
app.use('/x/authkit/*', authMiddleware(apiKeys));
52-
app.use('/organizations', authMiddleware(apiKeys));
53-
app.use('/organizations/*', authMiddleware(apiKeys));
54-
app.use('/organization_memberships', authMiddleware(apiKeys));
55-
app.use('/organization_memberships/*', authMiddleware(apiKeys));
56-
app.use('/organization_domains', authMiddleware(apiKeys));
57-
app.use('/organization_domains/*', authMiddleware(apiKeys));
58-
app.use('/connections', authMiddleware(apiKeys));
59-
app.use('/connections/*', authMiddleware(apiKeys));
60-
app.use('/directories', authMiddleware(apiKeys));
61-
app.use('/directories/*', authMiddleware(apiKeys));
62-
app.use('/directory_groups', authMiddleware(apiKeys));
63-
app.use('/directory_groups/*', authMiddleware(apiKeys));
64-
app.use('/directory_users', authMiddleware(apiKeys));
65-
app.use('/directory_users/*', authMiddleware(apiKeys));
66-
app.use('/events', authMiddleware(apiKeys));
67-
app.use('/events/*', authMiddleware(apiKeys));
68-
app.use('/pipes/*', authMiddleware(apiKeys));
69-
app.use('/audit_logs/*', authMiddleware(apiKeys));
70-
app.use('/feature-flags', authMiddleware(apiKeys));
71-
app.use('/feature-flags/*', authMiddleware(apiKeys));
72-
app.use('/connect/*', authMiddleware(apiKeys));
73-
app.use('/data-integrations/*', async (c, next) => {
74-
const path = new URL(c.req.url).pathname;
75-
if (path.endsWith('/authorize')) return next();
76-
return authMiddleware(apiKeys)(c, next);
60+
61+
return auth(c, next);
7762
});
78-
app.use('/radar/*', authMiddleware(apiKeys));
79-
app.use('/api_keys', authMiddleware(apiKeys));
80-
app.use('/api_keys/*', authMiddleware(apiKeys));
81-
app.use('/portal/*', authMiddleware(apiKeys));
82-
app.use('/webhook_endpoints', authMiddleware(apiKeys));
83-
app.use('/webhook_endpoints/*', authMiddleware(apiKeys));
84-
app.use('/auth/factors', authMiddleware(apiKeys));
85-
app.use('/auth/factors/*', authMiddleware(apiKeys));
86-
app.use('/auth/challenges/*', authMiddleware(apiKeys));
8763

8864
// Rate limiting
8965
const rateLimitCounters = new Map<string, { remaining: number; resetAt: number }>();

src/emulate/core/store.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export class Collection<T extends Entity> {
113113
return this.items.delete(id);
114114
}
115115

116+
deleteBy(field: keyof T, value: string | number): number {
117+
const items = this.findBy(field, value);
118+
for (const item of items) this.delete(item.id);
119+
return items.length;
120+
}
121+
116122
setHooks(hooks: CollectionHooks<T>): void {
117123
this.hooks = hooks;
118124
}
@@ -127,7 +133,11 @@ export class Collection<T extends Entity> {
127133

128134
count(filter?: FilterFn<T>): number {
129135
if (!filter) return this.items.size;
130-
return this.all().filter(filter).length;
136+
let n = 0;
137+
for (const item of this.items.values()) {
138+
if (filter(item)) n++;
139+
}
140+
return n;
131141
}
132142

133143
clear(): void {
@@ -168,6 +178,17 @@ export class Store {
168178
this._data.set(key, value);
169179
}
170180

181+
deleteDataByPrefix(prefix: string): number {
182+
let count = 0;
183+
for (const key of this._data.keys()) {
184+
if (key.startsWith(prefix)) {
185+
this._data.delete(key);
186+
count++;
187+
}
188+
}
189+
return count;
190+
}
191+
171192
reset(): void {
172193
for (const collection of this.collections.values()) {
173194
collection.clear();

src/emulate/workos/constants.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/** Typed keys for Store.getData/setData */
2+
export const STORE_KEYS = {
3+
workosStore: '_workos_store',
4+
eventBus: 'eventBus',
5+
apiKeyMap: 'apiKeyMap',
6+
jwtTemplate: 'jwt_template',
7+
} as const;
8+
9+
/** Prefix for dynamic store keys */
10+
export const STORE_KEY_PREFIXES = {
11+
pendingAuth: 'pending_auth:',
12+
ssoToken: 'sso_token:',
13+
ssoLogout: 'sso_logout:',
14+
auditSchema: 'audit_schema_',
15+
radarIpList: 'radar_ip_list',
16+
} as const;
17+
18+
/** All WorkOS webhook event names */
19+
export const EVENTS = {
20+
userCreated: 'user.created',
21+
userUpdated: 'user.updated',
22+
userDeleted: 'user.deleted',
23+
organizationCreated: 'organization.created',
24+
organizationUpdated: 'organization.updated',
25+
organizationDeleted: 'organization.deleted',
26+
organizationDomainCreated: 'organization_domain.created',
27+
organizationDomainVerified: 'organization_domain.verified',
28+
organizationDomainUpdated: 'organization_domain.updated',
29+
organizationDomainDeleted: 'organization_domain.deleted',
30+
organizationMembershipCreated: 'organization_membership.created',
31+
organizationMembershipUpdated: 'organization_membership.updated',
32+
organizationMembershipDeleted: 'organization_membership.deleted',
33+
connectionCreated: 'connection.created',
34+
connectionUpdated: 'connection.updated',
35+
connectionDeleted: 'connection.deleted',
36+
sessionCreated: 'session.created',
37+
sessionRevoked: 'session.revoked',
38+
invitationCreated: 'invitation.created',
39+
invitationAccepted: 'invitation.accepted',
40+
invitationRevoked: 'invitation.revoked',
41+
invitationResent: 'invitation.resent',
42+
roleCreated: 'role.created',
43+
roleUpdated: 'role.updated',
44+
roleDeleted: 'role.deleted',
45+
permissionCreated: 'permission.created',
46+
permissionUpdated: 'permission.updated',
47+
permissionDeleted: 'permission.deleted',
48+
directoryCreated: 'directory.created',
49+
directoryUpdated: 'directory.updated',
50+
directoryDeleted: 'directory.deleted',
51+
directoryUserCreated: 'directory_user.created',
52+
directoryUserUpdated: 'directory_user.updated',
53+
directoryUserDeleted: 'directory_user.deleted',
54+
directoryGroupCreated: 'directory_group.created',
55+
directoryGroupUpdated: 'directory_group.updated',
56+
directoryGroupDeleted: 'directory_group.deleted',
57+
} as const;
58+
59+
export type WorkOSEventName = (typeof EVENTS)[keyof typeof EVENTS];

src/emulate/workos/entities.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,6 @@ export interface WorkOSRoleAssignment extends Entity {
267267
role_id: string;
268268
}
269269

270-
// --- Phase 4: CRUD Domains ---
271-
272270
export interface WorkOSDirectory extends Entity {
273271
object: 'directory';
274272
name: string;

src/emulate/workos/event-bus.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ describe('EventBus', () => {
5454
description: null,
5555
});
5656

57+
bus.rebuildIndex();
5758
// This should not attempt delivery (no fetch error even though URL is unreachable)
5859
bus.emit({ event: 'user.created', data: {} });
5960
expect(ws.events.all()).toHaveLength(1);
@@ -70,6 +71,7 @@ describe('EventBus', () => {
7071
description: null,
7172
});
7273

74+
bus.rebuildIndex();
7375
// user.created should not match the endpoint's filter
7476
bus.emit({ event: 'user.created', data: {} });
7577
expect(ws.events.all()).toHaveLength(1);
@@ -93,6 +95,7 @@ describe('EventBus', () => {
9395
description: null,
9496
});
9597

98+
bus.rebuildIndex();
9699
bus.emit({ event: 'user.created', data: { id: 'user_1' } });
97100

98101
// Wait for async delivery
@@ -132,6 +135,7 @@ describe('EventBus', () => {
132135
description: null,
133136
});
134137

138+
bus.rebuildIndex();
135139
// emit() should return immediately (fire-and-forget)
136140
const start = Date.now();
137141
bus.emit({ event: 'user.created', data: {} });

0 commit comments

Comments
 (0)