@onebun/core 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
1
+ /**
2
+ * WebSocket Guards Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+ import type { WsClientData, WsHandlerMetadata } from './ws.types';
12
+
13
+ import {
14
+ WsAuthGuard,
15
+ WsPermissionGuard,
16
+ WsRoomGuard,
17
+ WsAnyPermissionGuard,
18
+ WsServiceGuard,
19
+ WsAllGuards,
20
+ WsAnyGuard,
21
+ WsExecutionContextImpl,
22
+ executeGuards,
23
+ createGuard,
24
+ } from './ws-guards';
25
+ import { WsHandlerType } from './ws.types';
26
+
27
+ describe('ws-guards', () => {
28
+ // Helper to create mock client data
29
+ const createClient = (overrides: Partial<WsClientData> = {}): WsClientData => ({
30
+ id: 'test-client',
31
+ rooms: [],
32
+ connectedAt: Date.now(),
33
+ auth: null,
34
+ metadata: {},
35
+ ...overrides,
36
+ });
37
+
38
+ // Helper to create mock handler metadata
39
+ const createHandler = (): WsHandlerMetadata => ({
40
+ type: WsHandlerType.MESSAGE,
41
+ pattern: 'test:*',
42
+ handler: 'handleTest',
43
+ params: [],
44
+ });
45
+
46
+ // Helper to create execution context
47
+ const createContext = (client: WsClientData) =>
48
+ new WsExecutionContextImpl(
49
+ client,
50
+ {} as never, // mock socket
51
+ {},
52
+ createHandler(),
53
+ {},
54
+ );
55
+
56
+ describe('WsAuthGuard', () => {
57
+ const guard = new WsAuthGuard();
58
+
59
+ it('should allow authenticated clients', () => {
60
+ const client = createClient({
61
+ auth: { authenticated: true },
62
+ });
63
+ const context = createContext(client);
64
+
65
+ expect(guard.canActivate(context)).toBe(true);
66
+ });
67
+
68
+ it('should reject unauthenticated clients', () => {
69
+ const client = createClient({
70
+ auth: { authenticated: false },
71
+ });
72
+ const context = createContext(client);
73
+
74
+ expect(guard.canActivate(context)).toBe(false);
75
+ });
76
+
77
+ it('should reject clients without auth data', () => {
78
+ const client = createClient({ auth: null });
79
+ const context = createContext(client);
80
+
81
+ expect(guard.canActivate(context)).toBe(false);
82
+ });
83
+ });
84
+
85
+ describe('WsPermissionGuard', () => {
86
+ it('should allow clients with required permission', () => {
87
+ const guard = new WsPermissionGuard('admin');
88
+ const client = createClient({
89
+ auth: { authenticated: true, permissions: ['admin', 'user'] },
90
+ });
91
+ const context = createContext(client);
92
+
93
+ expect(guard.canActivate(context)).toBe(true);
94
+ });
95
+
96
+ it('should reject clients without required permission', () => {
97
+ const guard = new WsPermissionGuard('admin');
98
+ const client = createClient({
99
+ auth: { authenticated: true, permissions: ['user'] },
100
+ });
101
+ const context = createContext(client);
102
+
103
+ expect(guard.canActivate(context)).toBe(false);
104
+ });
105
+
106
+ it('should require all permissions when array is provided', () => {
107
+ const guard = new WsPermissionGuard(['admin', 'moderator']);
108
+ const client = createClient({
109
+ auth: { authenticated: true, permissions: ['admin'] },
110
+ });
111
+ const context = createContext(client);
112
+
113
+ expect(guard.canActivate(context)).toBe(false);
114
+ });
115
+
116
+ it('should allow when client has all required permissions', () => {
117
+ const guard = new WsPermissionGuard(['admin', 'moderator']);
118
+ const client = createClient({
119
+ auth: { authenticated: true, permissions: ['admin', 'moderator', 'user'] },
120
+ });
121
+ const context = createContext(client);
122
+
123
+ expect(guard.canActivate(context)).toBe(true);
124
+ });
125
+ });
126
+
127
+ describe('WsRoomGuard', () => {
128
+ it('should allow clients in the required room', () => {
129
+ const guard = new WsRoomGuard('vip');
130
+ const client = createClient({
131
+ rooms: ['general', 'vip'],
132
+ });
133
+ const context = createContext(client);
134
+
135
+ expect(guard.canActivate(context)).toBe(true);
136
+ });
137
+
138
+ it('should reject clients not in the required room', () => {
139
+ const guard = new WsRoomGuard('vip');
140
+ const client = createClient({
141
+ rooms: ['general'],
142
+ });
143
+ const context = createContext(client);
144
+
145
+ expect(guard.canActivate(context)).toBe(false);
146
+ });
147
+ });
148
+
149
+ describe('WsAnyPermissionGuard', () => {
150
+ it('should allow clients with any of the permissions', () => {
151
+ const guard = new WsAnyPermissionGuard(['admin', 'moderator']);
152
+ const client = createClient({
153
+ auth: { authenticated: true, permissions: ['moderator'] },
154
+ });
155
+ const context = createContext(client);
156
+
157
+ expect(guard.canActivate(context)).toBe(true);
158
+ });
159
+
160
+ it('should reject clients without any of the permissions', () => {
161
+ const guard = new WsAnyPermissionGuard(['admin', 'moderator']);
162
+ const client = createClient({
163
+ auth: { authenticated: true, permissions: ['user'] },
164
+ });
165
+ const context = createContext(client);
166
+
167
+ expect(guard.canActivate(context)).toBe(false);
168
+ });
169
+ });
170
+
171
+ describe('WsServiceGuard', () => {
172
+ it('should allow allowed services', () => {
173
+ const guard = new WsServiceGuard('payment-service');
174
+ const client = createClient({
175
+ auth: { authenticated: true, serviceId: 'payment-service' },
176
+ });
177
+ const context = createContext(client);
178
+
179
+ expect(guard.canActivate(context)).toBe(true);
180
+ });
181
+
182
+ it('should reject non-allowed services', () => {
183
+ const guard = new WsServiceGuard('payment-service');
184
+ const client = createClient({
185
+ auth: { authenticated: true, serviceId: 'other-service' },
186
+ });
187
+ const context = createContext(client);
188
+
189
+ expect(guard.canActivate(context)).toBe(false);
190
+ });
191
+
192
+ it('should allow multiple services', () => {
193
+ const guard = new WsServiceGuard(['payment-service', 'billing-service']);
194
+ const client = createClient({
195
+ auth: { authenticated: true, serviceId: 'billing-service' },
196
+ });
197
+ const context = createContext(client);
198
+
199
+ expect(guard.canActivate(context)).toBe(true);
200
+ });
201
+ });
202
+
203
+ describe('WsAllGuards', () => {
204
+ it('should pass when all guards pass', async () => {
205
+ const guard = new WsAllGuards([new WsAuthGuard(), new WsPermissionGuard('admin')]);
206
+
207
+ const client = createClient({
208
+ auth: { authenticated: true, permissions: ['admin'] },
209
+ });
210
+ const context = createContext(client);
211
+
212
+ expect(await guard.canActivate(context)).toBe(true);
213
+ });
214
+
215
+ it('should fail when any guard fails', async () => {
216
+ const guard = new WsAllGuards([new WsAuthGuard(), new WsPermissionGuard('admin')]);
217
+
218
+ const client = createClient({
219
+ auth: { authenticated: true, permissions: ['user'] },
220
+ });
221
+ const context = createContext(client);
222
+
223
+ expect(await guard.canActivate(context)).toBe(false);
224
+ });
225
+ });
226
+
227
+ describe('WsAnyGuard', () => {
228
+ it('should pass when any guard passes', async () => {
229
+ const guard = new WsAnyGuard([new WsPermissionGuard('admin'), new WsRoomGuard('vip')]);
230
+
231
+ const client = createClient({
232
+ auth: { authenticated: true, permissions: ['user'] },
233
+ rooms: ['vip'],
234
+ });
235
+ const context = createContext(client);
236
+
237
+ expect(await guard.canActivate(context)).toBe(true);
238
+ });
239
+
240
+ it('should fail when all guards fail', async () => {
241
+ const guard = new WsAnyGuard([new WsPermissionGuard('admin'), new WsRoomGuard('vip')]);
242
+
243
+ const client = createClient({
244
+ auth: { authenticated: true, permissions: ['user'] },
245
+ rooms: ['general'],
246
+ });
247
+ const context = createContext(client);
248
+
249
+ expect(await guard.canActivate(context)).toBe(false);
250
+ });
251
+ });
252
+
253
+ describe('executeGuards', () => {
254
+ it('should execute guard classes', async () => {
255
+ const client = createClient({
256
+ auth: { authenticated: true },
257
+ });
258
+ const context = createContext(client);
259
+
260
+ const result = await executeGuards([WsAuthGuard], context);
261
+ expect(result).toBe(true);
262
+ });
263
+
264
+ it('should execute guard instances', async () => {
265
+ const client = createClient({
266
+ auth: { authenticated: true, permissions: ['admin'] },
267
+ });
268
+ const context = createContext(client);
269
+
270
+ const result = await executeGuards([new WsPermissionGuard('admin')], context);
271
+ expect(result).toBe(true);
272
+ });
273
+
274
+ it('should return true when no guards', async () => {
275
+ const context = createContext(createClient());
276
+ const result = await executeGuards([], context);
277
+ expect(result).toBe(true);
278
+ });
279
+ });
280
+
281
+ describe('createGuard', () => {
282
+ it('should create custom guard from function', () => {
283
+ // eslint-disable-next-line @typescript-eslint/naming-convention
284
+ const CustomGuard = createGuard((ctx) => {
285
+ return (ctx.getClient().metadata as { customCheck?: boolean }).customCheck === true;
286
+ });
287
+
288
+ const guard = new CustomGuard();
289
+ const client = createClient({
290
+ metadata: { customCheck: true },
291
+ });
292
+ const context = createContext(client);
293
+
294
+ expect(guard.canActivate(context)).toBe(true);
295
+ });
296
+ });
297
+
298
+ describe('WsExecutionContextImpl', () => {
299
+ it('should provide access to client data', () => {
300
+ const client = createClient({ id: 'test-id' });
301
+ const context = createContext(client);
302
+
303
+ expect(context.getClient().id).toBe('test-id');
304
+ });
305
+
306
+ it('should provide access to message data', () => {
307
+ const client = createClient();
308
+ const data = { text: 'hello' };
309
+ const context = new WsExecutionContextImpl(
310
+ client,
311
+ {} as never,
312
+ data,
313
+ createHandler(),
314
+ {},
315
+ );
316
+
317
+ expect(context.getData<{ text: string }>()).toEqual(data);
318
+ });
319
+
320
+ it('should provide access to pattern params', () => {
321
+ const client = createClient();
322
+ const params = { roomId: '123' };
323
+ const context = new WsExecutionContextImpl(
324
+ client,
325
+ {} as never,
326
+ {},
327
+ createHandler(),
328
+ params,
329
+ );
330
+
331
+ expect(context.getPatternParams()).toEqual(params);
332
+ });
333
+ });
334
+ });
@@ -0,0 +1,298 @@
1
+ /**
2
+ * WebSocket Guards
3
+ *
4
+ * Guards for authorizing WebSocket connections and message handling.
5
+ */
6
+
7
+ import type {
8
+ WsClientData,
9
+ WsGuard,
10
+ WsExecutionContext,
11
+ WsHandlerMetadata,
12
+ } from './ws.types';
13
+ import type { ServerWebSocket } from 'bun';
14
+
15
+
16
+ // ============================================================================
17
+ // Execution Context Implementation
18
+ // ============================================================================
19
+
20
+ /**
21
+ * Implementation of WsExecutionContext
22
+ */
23
+ export class WsExecutionContextImpl implements WsExecutionContext {
24
+ constructor(
25
+ private client: WsClientData,
26
+ private socket: ServerWebSocket<WsClientData>,
27
+ private data: unknown,
28
+ private handler: WsHandlerMetadata,
29
+ private patternParams: Record<string, string>,
30
+ ) {}
31
+
32
+ getClient(): WsClientData {
33
+ return this.client;
34
+ }
35
+
36
+ getSocket(): ServerWebSocket<WsClientData> {
37
+ return this.socket;
38
+ }
39
+
40
+ getData<T = unknown>(): T {
41
+ return this.data as T;
42
+ }
43
+
44
+ getHandler(): WsHandlerMetadata {
45
+ return this.handler;
46
+ }
47
+
48
+ getPatternParams(): Record<string, string> {
49
+ return this.patternParams;
50
+ }
51
+ }
52
+
53
+ // ============================================================================
54
+ // Built-in Guards
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Guard that requires client to be authenticated
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * @UseWsGuards(WsAuthGuard)
63
+ * @OnMessage('protected:*')
64
+ * handleProtectedMessage(@Client() client: WsClientData) {
65
+ * // Only authenticated clients can reach here
66
+ * }
67
+ * ```
68
+ */
69
+ export class WsAuthGuard implements WsGuard {
70
+ canActivate(context: WsExecutionContext): boolean {
71
+ const client = context.getClient();
72
+
73
+ return client.auth?.authenticated === true;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Guard that requires specific permission(s)
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * // Single permission
83
+ * @UseWsGuards(new WsPermissionGuard('admin'))
84
+ * @OnMessage('admin:*')
85
+ * handleAdminMessage() { }
86
+ *
87
+ * // Multiple permissions (all required)
88
+ * @UseWsGuards(new WsPermissionGuard(['admin', 'moderator']))
89
+ * @OnMessage('super:*')
90
+ * handleSuperMessage() { }
91
+ * ```
92
+ */
93
+ export class WsPermissionGuard implements WsGuard {
94
+ private requiredPermissions: string[];
95
+
96
+ constructor(permissions: string | string[]) {
97
+ this.requiredPermissions = Array.isArray(permissions) ? permissions : [permissions];
98
+ }
99
+
100
+ canActivate(context: WsExecutionContext): boolean {
101
+ const client = context.getClient();
102
+ const clientPermissions = client.auth?.permissions || [];
103
+
104
+ return this.requiredPermissions.every((p) => clientPermissions.includes(p));
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Guard that requires client to be in a specific room
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * @UseWsGuards(new WsRoomGuard('vip'))
114
+ * @OnMessage('vip:*')
115
+ * handleVipMessage() { }
116
+ * ```
117
+ */
118
+ export class WsRoomGuard implements WsGuard {
119
+ constructor(private roomName: string) {}
120
+
121
+ canActivate(context: WsExecutionContext): boolean {
122
+ const client = context.getClient();
123
+
124
+ return client.rooms.includes(this.roomName);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Guard that allows any of multiple permissions
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * @UseWsGuards(new WsAnyPermissionGuard(['admin', 'moderator']))
134
+ * @OnMessage('manage:*')
135
+ * handleManageMessage() { }
136
+ * ```
137
+ */
138
+ export class WsAnyPermissionGuard implements WsGuard {
139
+ private permissions: string[];
140
+
141
+ constructor(permissions: string[]) {
142
+ this.permissions = permissions;
143
+ }
144
+
145
+ canActivate(context: WsExecutionContext): boolean {
146
+ const client = context.getClient();
147
+ const clientPermissions = client.auth?.permissions || [];
148
+
149
+ return this.permissions.some((p) => clientPermissions.includes(p));
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Guard that requires a specific service ID (for inter-service communication)
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * @UseWsGuards(new WsServiceGuard('payment-service'))
159
+ * @OnMessage('payment:webhook')
160
+ * handlePaymentWebhook() { }
161
+ * ```
162
+ */
163
+ export class WsServiceGuard implements WsGuard {
164
+ private allowedServices: string[];
165
+
166
+ constructor(services: string | string[]) {
167
+ this.allowedServices = Array.isArray(services) ? services : [services];
168
+ }
169
+
170
+ canActivate(context: WsExecutionContext): boolean {
171
+ const client = context.getClient();
172
+ const serviceId = client.auth?.serviceId;
173
+
174
+ if (!serviceId) {
175
+ return false;
176
+ }
177
+
178
+ return this.allowedServices.includes(serviceId);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Composite guard that requires all guards to pass
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * @UseWsGuards(new WsAllGuards([
188
+ * new WsAuthGuard(),
189
+ * new WsPermissionGuard('admin')
190
+ * ]))
191
+ * @OnMessage('admin:*')
192
+ * handleAdminMessage() { }
193
+ * ```
194
+ */
195
+ export class WsAllGuards implements WsGuard {
196
+ constructor(private guards: WsGuard[]) {}
197
+
198
+ async canActivate(context: WsExecutionContext): Promise<boolean> {
199
+ for (const guard of this.guards) {
200
+ const result = await guard.canActivate(context);
201
+ if (!result) {
202
+ return false;
203
+ }
204
+ }
205
+
206
+ return true;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Composite guard that requires any guard to pass
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * @UseWsGuards(new WsAnyGuard([
216
+ * new WsPermissionGuard('admin'),
217
+ * new WsServiceGuard('internal-service')
218
+ * ]))
219
+ * @OnMessage('internal:*')
220
+ * handleInternalMessage() { }
221
+ * ```
222
+ */
223
+ export class WsAnyGuard implements WsGuard {
224
+ constructor(private guards: WsGuard[]) {}
225
+
226
+ async canActivate(context: WsExecutionContext): Promise<boolean> {
227
+ for (const guard of this.guards) {
228
+ const result = await guard.canActivate(context);
229
+ if (result) {
230
+ return true;
231
+ }
232
+ }
233
+
234
+ return false;
235
+ }
236
+ }
237
+
238
+ // ============================================================================
239
+ // Guard Execution Helper
240
+ // ============================================================================
241
+
242
+ /**
243
+ * Execute a list of guards
244
+ *
245
+ * @param guards - Array of guard classes or instances
246
+ * @param context - Execution context
247
+ * @returns Whether all guards passed
248
+ */
249
+ export async function executeGuards(
250
+ guards: (Function | WsGuard)[],
251
+ context: WsExecutionContext,
252
+ ): Promise<boolean> {
253
+ for (const guard of guards) {
254
+ let guardInstance: WsGuard;
255
+
256
+ // Check if it's already an instance
257
+ if (typeof guard === 'function') {
258
+ // Create instance from class
259
+ guardInstance = new (guard as new () => WsGuard)();
260
+ } else {
261
+ guardInstance = guard;
262
+ }
263
+
264
+ const result = await guardInstance.canActivate(context);
265
+ if (!result) {
266
+ return false;
267
+ }
268
+ }
269
+
270
+ return true;
271
+ }
272
+
273
+ /**
274
+ * Create a custom guard from a function
275
+ *
276
+ * @param fn - Guard function
277
+ * @returns Guard class
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * const CustomGuard = createGuard((ctx) => {
282
+ * return ctx.getClient().metadata.customCheck === true;
283
+ * });
284
+ *
285
+ * @UseWsGuards(CustomGuard)
286
+ * @OnMessage('custom:*')
287
+ * handleCustomMessage() { }
288
+ * ```
289
+ */
290
+ export function createGuard(
291
+ fn: (context: WsExecutionContext) => boolean | Promise<boolean>,
292
+ ): new () => WsGuard {
293
+ return class implements WsGuard {
294
+ canActivate(context: WsExecutionContext): boolean | Promise<boolean> {
295
+ return fn(context);
296
+ }
297
+ };
298
+ }