@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.
- package/README.md +233 -0
- package/package.json +1 -1
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +753 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -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
|
+
});
|
package/src/ws-guards.ts
ADDED
|
@@ -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
|
+
}
|