@onebun/core 0.1.2 → 0.1.4
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/package.json +6 -6
- package/src/{application.test.ts → application/application.test.ts} +6 -5
- package/src/{application.ts → application/application.ts} +131 -12
- package/src/application/index.ts +9 -0
- package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
- package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
- package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
- package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
- package/src/{decorators.ts → decorators/decorators.ts} +3 -2
- package/src/decorators/index.ts +15 -0
- package/src/index.ts +47 -134
- package/src/module/index.ts +12 -0
- package/src/{module.test.ts → module/module.test.ts} +3 -2
- package/src/{module.ts → module/module.ts} +6 -5
- package/src/queue/adapters/index.ts +8 -0
- package/src/queue/adapters/memory.adapter.test.ts +405 -0
- package/src/queue/adapters/memory.adapter.ts +509 -0
- package/src/queue/adapters/redis.adapter.ts +673 -0
- package/src/queue/cron-expression.test.ts +145 -0
- package/src/queue/cron-expression.ts +115 -0
- package/src/queue/cron-parser.test.ts +185 -0
- package/src/queue/cron-parser.ts +287 -0
- package/src/queue/decorators.test.ts +292 -0
- package/src/queue/decorators.ts +493 -0
- package/src/queue/docs-examples.test.ts +449 -0
- package/src/queue/guards.test.ts +309 -0
- package/src/queue/guards.ts +307 -0
- package/src/queue/index.ts +118 -0
- package/src/queue/pattern-matcher.test.ts +191 -0
- package/src/queue/pattern-matcher.ts +252 -0
- package/src/queue/queue.service.ts +421 -0
- package/src/queue/scheduler.test.ts +235 -0
- package/src/queue/scheduler.ts +379 -0
- package/src/queue/types.ts +502 -0
- package/src/redis/index.ts +8 -0
- package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
- package/src/service-client/index.ts +10 -0
- package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
- package/src/{service-client.ts → service-client/service-client.ts} +1 -1
- package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
- package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
- package/src/testing/index.ts +7 -0
- package/src/types.ts +34 -5
- package/src/websocket/index.ts +50 -0
- package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
- package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
- package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
- package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
- /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
- /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
- /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
- /package/src/{config.service.ts → module/config.service.ts} +0 -0
- /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
- /package/src/{controller.ts → module/controller.ts} +0 -0
- /package/src/{service.test.ts → module/service.test.ts} +0 -0
- /package/src/{service.ts → module/service.ts} +0 -0
- /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
- /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
- /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
- /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
- /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
- /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
- /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
- /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
- /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
- /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
- /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
- /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
- /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
- /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
- /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
- /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
- /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
- /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
- /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
- /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
- /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
- /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
- /package/src/{ws.types.ts → websocket/ws.types.ts} +0 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Guards Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
Message,
|
|
13
|
+
MessageMetadata,
|
|
14
|
+
MessageGuard,
|
|
15
|
+
MessageExecutionContext,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
MessageExecutionContextImpl,
|
|
20
|
+
MessageAuthGuard,
|
|
21
|
+
MessageServiceGuard,
|
|
22
|
+
MessageHeaderGuard,
|
|
23
|
+
MessageTraceGuard,
|
|
24
|
+
MessageAllGuards,
|
|
25
|
+
MessageAnyGuard,
|
|
26
|
+
executeMessageGuards,
|
|
27
|
+
createMessageGuard,
|
|
28
|
+
} from './guards';
|
|
29
|
+
|
|
30
|
+
// Helper to create a mock message
|
|
31
|
+
function createMockMessage(metadata: MessageMetadata = {}): Message {
|
|
32
|
+
return {
|
|
33
|
+
id: 'test-id',
|
|
34
|
+
pattern: 'test.pattern',
|
|
35
|
+
data: { test: true },
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
metadata,
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
39
|
+
async ack() {},
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
41
|
+
async nack() {},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Helper to create execution context
|
|
46
|
+
function createContext(metadata: MessageMetadata = {}): MessageExecutionContext {
|
|
47
|
+
const message = createMockMessage(metadata);
|
|
48
|
+
|
|
49
|
+
return new MessageExecutionContextImpl(
|
|
50
|
+
message,
|
|
51
|
+
'test.pattern',
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
53
|
+
() => {},
|
|
54
|
+
class TestHandler {},
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('queue-guards', () => {
|
|
59
|
+
describe('MessageExecutionContextImpl', () => {
|
|
60
|
+
it('should provide message access', () => {
|
|
61
|
+
const message = createMockMessage({ authorization: 'Bearer token' });
|
|
62
|
+
const context = new MessageExecutionContextImpl(
|
|
63
|
+
message,
|
|
64
|
+
'test.pattern',
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
66
|
+
() => {},
|
|
67
|
+
class TestHandler {},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(context.getMessage()).toBe(message);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should provide metadata access', () => {
|
|
74
|
+
const context = createContext({ authorization: 'Bearer token' });
|
|
75
|
+
const metadata = context.getMetadata();
|
|
76
|
+
|
|
77
|
+
expect(metadata.authorization).toBe('Bearer token');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should provide pattern access', () => {
|
|
81
|
+
const context = createContext();
|
|
82
|
+
expect(context.getPattern()).toBe('test.pattern');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should provide handler access', () => {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
87
|
+
const handler = () => {};
|
|
88
|
+
const message = createMockMessage();
|
|
89
|
+
const context = new MessageExecutionContextImpl(
|
|
90
|
+
message,
|
|
91
|
+
'test.pattern',
|
|
92
|
+
handler,
|
|
93
|
+
class TestHandler {},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(context.getHandler()).toBe(handler);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should provide class access', () => {
|
|
100
|
+
class TestHandler {}
|
|
101
|
+
const message = createMockMessage();
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
103
|
+
const context = new MessageExecutionContextImpl(message, 'test.pattern', () => {}, TestHandler);
|
|
104
|
+
|
|
105
|
+
expect(context.getClass()).toBe(TestHandler);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('MessageAuthGuard', () => {
|
|
110
|
+
it('should pass when authorization is present', () => {
|
|
111
|
+
const guard = new MessageAuthGuard();
|
|
112
|
+
const context = createContext({ authorization: 'Bearer token123' });
|
|
113
|
+
|
|
114
|
+
expect(guard.canActivate(context)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should fail when authorization is missing', () => {
|
|
118
|
+
const guard = new MessageAuthGuard();
|
|
119
|
+
const context = createContext({});
|
|
120
|
+
|
|
121
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should fail when authorization is empty', () => {
|
|
125
|
+
const guard = new MessageAuthGuard();
|
|
126
|
+
const context = createContext({ authorization: '' });
|
|
127
|
+
|
|
128
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('MessageServiceGuard', () => {
|
|
133
|
+
it('should pass for allowed service', () => {
|
|
134
|
+
const guard = new MessageServiceGuard(['payment-service', 'order-service']);
|
|
135
|
+
const context = createContext({ serviceId: 'payment-service' });
|
|
136
|
+
|
|
137
|
+
expect(guard.canActivate(context)).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should fail for disallowed service', () => {
|
|
141
|
+
const guard = new MessageServiceGuard(['payment-service']);
|
|
142
|
+
const context = createContext({ serviceId: 'unknown-service' });
|
|
143
|
+
|
|
144
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should fail when serviceId is missing', () => {
|
|
148
|
+
const guard = new MessageServiceGuard(['payment-service']);
|
|
149
|
+
const context = createContext({});
|
|
150
|
+
|
|
151
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
156
|
+
describe('MessageHeaderGuard', () => {
|
|
157
|
+
it('should pass when header is present', () => {
|
|
158
|
+
const guard = new MessageHeaderGuard('x-api-key');
|
|
159
|
+
const context = createContext({ headers: { 'x-api-key': 'secret' } });
|
|
160
|
+
|
|
161
|
+
expect(guard.canActivate(context)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should fail when header is missing', () => {
|
|
165
|
+
const guard = new MessageHeaderGuard('x-api-key');
|
|
166
|
+
const context = createContext({ headers: {} });
|
|
167
|
+
|
|
168
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should fail when headers object is missing', () => {
|
|
172
|
+
const guard = new MessageHeaderGuard('x-api-key');
|
|
173
|
+
const context = createContext({});
|
|
174
|
+
|
|
175
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should check expected value when provided', () => {
|
|
179
|
+
const guard = new MessageHeaderGuard('x-api-key', 'expected-value');
|
|
180
|
+
const contextMatch = createContext({ headers: { 'x-api-key': 'expected-value' } });
|
|
181
|
+
const contextNoMatch = createContext({ headers: { 'x-api-key': 'wrong-value' } });
|
|
182
|
+
|
|
183
|
+
expect(guard.canActivate(contextMatch)).toBe(true);
|
|
184
|
+
expect(guard.canActivate(contextNoMatch)).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('MessageTraceGuard', () => {
|
|
190
|
+
it('should pass when traceId is present', () => {
|
|
191
|
+
const guard = new MessageTraceGuard();
|
|
192
|
+
const context = createContext({ traceId: 'trace-123' });
|
|
193
|
+
|
|
194
|
+
expect(guard.canActivate(context)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should fail when traceId is missing', () => {
|
|
198
|
+
const guard = new MessageTraceGuard();
|
|
199
|
+
const context = createContext({});
|
|
200
|
+
|
|
201
|
+
expect(guard.canActivate(context)).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('MessageAllGuards', () => {
|
|
206
|
+
it('should pass when all guards pass', async () => {
|
|
207
|
+
const guard = new MessageAllGuards([MessageAuthGuard, MessageTraceGuard]);
|
|
208
|
+
const context = createContext({ authorization: 'Bearer token', traceId: 'trace-123' });
|
|
209
|
+
|
|
210
|
+
const result = await guard.canActivate(context);
|
|
211
|
+
expect(result).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should fail when any guard fails', async () => {
|
|
215
|
+
const guard = new MessageAllGuards([MessageAuthGuard, MessageTraceGuard]);
|
|
216
|
+
const context = createContext({ authorization: 'Bearer token' }); // No traceId
|
|
217
|
+
|
|
218
|
+
const result = await guard.canActivate(context);
|
|
219
|
+
expect(result).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should accept guard instances', async () => {
|
|
223
|
+
const serviceGuard = new MessageServiceGuard(['allowed-service']);
|
|
224
|
+
const guard = new MessageAllGuards([serviceGuard, MessageAuthGuard]);
|
|
225
|
+
const context = createContext({ serviceId: 'allowed-service', authorization: 'Bearer token' });
|
|
226
|
+
|
|
227
|
+
const result = await guard.canActivate(context);
|
|
228
|
+
expect(result).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('MessageAnyGuard', () => {
|
|
233
|
+
it('should pass when any guard passes', async () => {
|
|
234
|
+
const guard = new MessageAnyGuard([MessageAuthGuard, MessageTraceGuard]);
|
|
235
|
+
const context = createContext({ authorization: 'Bearer token' }); // Only auth, no trace
|
|
236
|
+
|
|
237
|
+
const result = await guard.canActivate(context);
|
|
238
|
+
expect(result).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should fail when all guards fail', async () => {
|
|
242
|
+
const guard = new MessageAnyGuard([MessageAuthGuard, MessageTraceGuard]);
|
|
243
|
+
const context = createContext({}); // No auth, no trace
|
|
244
|
+
|
|
245
|
+
const result = await guard.canActivate(context);
|
|
246
|
+
expect(result).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('executeMessageGuards', () => {
|
|
251
|
+
it('should execute all guards and return true when all pass', async () => {
|
|
252
|
+
const context = createContext({ authorization: 'Bearer token' });
|
|
253
|
+
const result = await executeMessageGuards([MessageAuthGuard], context);
|
|
254
|
+
|
|
255
|
+
expect(result).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should return false when any guard fails', async () => {
|
|
259
|
+
const context = createContext({});
|
|
260
|
+
const result = await executeMessageGuards([MessageAuthGuard], context);
|
|
261
|
+
|
|
262
|
+
expect(result).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should short-circuit on first failure', async () => {
|
|
266
|
+
let secondGuardCalled = false;
|
|
267
|
+
|
|
268
|
+
class SecondGuard implements MessageGuard {
|
|
269
|
+
canActivate(): boolean {
|
|
270
|
+
secondGuardCalled = true;
|
|
271
|
+
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const context = createContext({}); // Will fail auth guard
|
|
277
|
+
await executeMessageGuards([MessageAuthGuard, SecondGuard], context);
|
|
278
|
+
|
|
279
|
+
expect(secondGuardCalled).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('createMessageGuard', () => {
|
|
284
|
+
it('should create guard from function', () => {
|
|
285
|
+
const guard = createMessageGuard((context) => {
|
|
286
|
+
return context.getMetadata().authorization === 'secret';
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const passContext = createContext({ authorization: 'secret' });
|
|
290
|
+
const failContext = createContext({ authorization: 'wrong' });
|
|
291
|
+
|
|
292
|
+
expect(guard.canActivate(passContext)).toBe(true);
|
|
293
|
+
expect(guard.canActivate(failContext)).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should support async functions', async () => {
|
|
297
|
+
const guard = createMessageGuard(async (context) => {
|
|
298
|
+
await Promise.resolve(); // Simulate async operation
|
|
299
|
+
|
|
300
|
+
return !!context.getMetadata().authorization;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const context = createContext({ authorization: 'token' });
|
|
304
|
+
const result = await guard.canActivate(context);
|
|
305
|
+
|
|
306
|
+
expect(result).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Guards
|
|
3
|
+
*
|
|
4
|
+
* Guards for message handlers, similar to WebSocket guards.
|
|
5
|
+
* Guards can be used to authorize message processing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Message,
|
|
10
|
+
MessageMetadata,
|
|
11
|
+
MessageGuard,
|
|
12
|
+
MessageExecutionContext,
|
|
13
|
+
MessageGuardConstructor,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Execution Context Implementation
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Implementation of MessageExecutionContext
|
|
22
|
+
*/
|
|
23
|
+
export class MessageExecutionContextImpl implements MessageExecutionContext {
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly message: Message,
|
|
26
|
+
private readonly pattern: string,
|
|
27
|
+
private readonly handler: (...args: unknown[]) => unknown,
|
|
28
|
+
private readonly targetClass: new (...args: unknown[]) => unknown,
|
|
29
|
+
) {}
|
|
30
|
+
|
|
31
|
+
getMessage<T>(): Message<T> {
|
|
32
|
+
return this.message as Message<T>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getMetadata(): MessageMetadata {
|
|
36
|
+
return this.message.metadata;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getPattern(): string {
|
|
40
|
+
return this.pattern;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getHandler(): (...args: unknown[]) => unknown {
|
|
44
|
+
return this.handler;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getClass(): new (...args: unknown[]) => unknown {
|
|
48
|
+
return this.targetClass;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Built-in Guards
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Guard that checks for the presence of an authorization token
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* @UseMessageGuards(MessageAuthGuard)
|
|
62
|
+
* @Subscribe('orders.*')
|
|
63
|
+
* async handleOrder(message: Message<OrderData>) {
|
|
64
|
+
* // Only messages with authorization token will be processed
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class MessageAuthGuard implements MessageGuard {
|
|
69
|
+
canActivate(context: MessageExecutionContext): boolean {
|
|
70
|
+
const metadata = context.getMetadata();
|
|
71
|
+
|
|
72
|
+
return !!metadata.authorization;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Guard that checks if the message comes from an allowed service
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const serviceGuard = new MessageServiceGuard(['payment-service', 'order-service']);
|
|
82
|
+
*
|
|
83
|
+
* @UseMessageGuards(serviceGuard)
|
|
84
|
+
* @Subscribe('events.internal.*')
|
|
85
|
+
* async handleInternalEvent(message: Message<EventData>) {
|
|
86
|
+
* // Only messages from allowed services will be processed
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export class MessageServiceGuard implements MessageGuard {
|
|
91
|
+
constructor(private readonly allowedServices: string[]) {}
|
|
92
|
+
|
|
93
|
+
canActivate(context: MessageExecutionContext): boolean {
|
|
94
|
+
const metadata = context.getMetadata();
|
|
95
|
+
const serviceId = metadata.serviceId;
|
|
96
|
+
|
|
97
|
+
if (!serviceId) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return this.allowedServices.includes(serviceId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Guard that requires a specific header to be present
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const headerGuard = new MessageHeaderGuard('x-api-key');
|
|
111
|
+
*
|
|
112
|
+
* @UseMessageGuards(headerGuard)
|
|
113
|
+
* @Subscribe('api.*')
|
|
114
|
+
* async handleApiRequest(message: Message<RequestData>) {
|
|
115
|
+
* // Only messages with x-api-key header will be processed
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export class MessageHeaderGuard implements MessageGuard {
|
|
120
|
+
constructor(
|
|
121
|
+
private readonly headerName: string,
|
|
122
|
+
private readonly expectedValue?: string,
|
|
123
|
+
) {}
|
|
124
|
+
|
|
125
|
+
canActivate(context: MessageExecutionContext): boolean {
|
|
126
|
+
const metadata = context.getMetadata();
|
|
127
|
+
const headers = metadata.headers;
|
|
128
|
+
|
|
129
|
+
if (!headers) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const value = headers[this.headerName];
|
|
134
|
+
|
|
135
|
+
if (value === undefined) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this.expectedValue !== undefined) {
|
|
140
|
+
return value === this.expectedValue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Guard that checks for trace context (for distributed tracing requirements)
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* @UseMessageGuards(MessageTraceGuard)
|
|
153
|
+
* @Subscribe('traced.*')
|
|
154
|
+
* async handleTracedEvent(message: Message<EventData>) {
|
|
155
|
+
* // Only messages with trace context will be processed
|
|
156
|
+
* }
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export class MessageTraceGuard implements MessageGuard {
|
|
160
|
+
canActivate(context: MessageExecutionContext): boolean {
|
|
161
|
+
const metadata = context.getMetadata();
|
|
162
|
+
|
|
163
|
+
return !!metadata.traceId;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// Composite Guards
|
|
169
|
+
// ============================================================================
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Guard that passes if ALL of the provided guards pass
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const allGuards = new MessageAllGuards([
|
|
177
|
+
* MessageAuthGuard,
|
|
178
|
+
* new MessageServiceGuard(['payment-service']),
|
|
179
|
+
* ]);
|
|
180
|
+
*
|
|
181
|
+
* @UseMessageGuards(allGuards)
|
|
182
|
+
* @Subscribe('secure.*')
|
|
183
|
+
* async handleSecureEvent(message: Message<EventData>) {
|
|
184
|
+
* // Requires both auth and service check to pass
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export class MessageAllGuards implements MessageGuard {
|
|
189
|
+
private readonly guards: MessageGuard[];
|
|
190
|
+
|
|
191
|
+
constructor(guards: Array<MessageGuard | MessageGuardConstructor>) {
|
|
192
|
+
this.guards = guards.map((guard) => {
|
|
193
|
+
if (typeof guard === 'function') {
|
|
194
|
+
return new guard();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return guard;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async canActivate(context: MessageExecutionContext): Promise<boolean> {
|
|
202
|
+
for (const guard of this.guards) {
|
|
203
|
+
const result = await guard.canActivate(context);
|
|
204
|
+
if (!result) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Guard that passes if ANY of the provided guards passes
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const anyGuard = new MessageAnyGuard([
|
|
219
|
+
* new MessageServiceGuard(['internal-service']),
|
|
220
|
+
* MessageAuthGuard,
|
|
221
|
+
* ]);
|
|
222
|
+
*
|
|
223
|
+
* @UseMessageGuards(anyGuard)
|
|
224
|
+
* @Subscribe('events.*')
|
|
225
|
+
* async handleEvent(message: Message<EventData>) {
|
|
226
|
+
* // Requires either service check OR auth to pass
|
|
227
|
+
* }
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
export class MessageAnyGuard implements MessageGuard {
|
|
231
|
+
private readonly guards: MessageGuard[];
|
|
232
|
+
|
|
233
|
+
constructor(guards: Array<MessageGuard | MessageGuardConstructor>) {
|
|
234
|
+
this.guards = guards.map((guard) => {
|
|
235
|
+
if (typeof guard === 'function') {
|
|
236
|
+
return new guard();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return guard;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async canActivate(context: MessageExecutionContext): Promise<boolean> {
|
|
244
|
+
for (const guard of this.guards) {
|
|
245
|
+
const result = await guard.canActivate(context);
|
|
246
|
+
if (result) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================================================
|
|
256
|
+
// Guard Execution
|
|
257
|
+
// ============================================================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Execute an array of guards and return whether all passed
|
|
261
|
+
*
|
|
262
|
+
* @param guards - Array of guard instances or constructors
|
|
263
|
+
* @param context - Message execution context
|
|
264
|
+
* @returns Whether all guards passed
|
|
265
|
+
*/
|
|
266
|
+
export async function executeMessageGuards(
|
|
267
|
+
guards: Array<MessageGuard | MessageGuardConstructor>,
|
|
268
|
+
context: MessageExecutionContext,
|
|
269
|
+
): Promise<boolean> {
|
|
270
|
+
for (const guard of guards) {
|
|
271
|
+
const guardInstance = typeof guard === 'function' ? new guard() : guard;
|
|
272
|
+
const result = await guardInstance.canActivate(context);
|
|
273
|
+
if (!result) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Create a guard from a simple check function
|
|
283
|
+
*
|
|
284
|
+
* @param checkFn - Function that returns whether the message should be processed
|
|
285
|
+
* @returns MessageGuard instance
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* const customGuard = createMessageGuard((context) => {
|
|
290
|
+
* const metadata = context.getMetadata();
|
|
291
|
+
* return metadata.headers?.['x-custom-header'] === 'expected-value';
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* @UseMessageGuards(customGuard)
|
|
295
|
+
* @Subscribe('custom.*')
|
|
296
|
+
* async handleCustomEvent(message: Message<EventData>) {
|
|
297
|
+
* // Custom guard logic
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*/
|
|
301
|
+
export function createMessageGuard(
|
|
302
|
+
checkFn: (context: MessageExecutionContext) => boolean | Promise<boolean>,
|
|
303
|
+
): MessageGuard {
|
|
304
|
+
return {
|
|
305
|
+
canActivate: checkFn,
|
|
306
|
+
};
|
|
307
|
+
}
|