@martel/calyx 1.4.0 → 1.6.0
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/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/src/config/config.module.ts +61 -0
- package/src/config/config.service.ts +24 -0
- package/src/config/index.ts +2 -0
- package/src/event-emitter/decorators.ts +10 -0
- package/src/event-emitter/event-emitter.module.ts +17 -0
- package/src/event-emitter/event-emitter.ts +61 -0
- package/src/event-emitter/index.ts +3 -0
- package/src/http/application.ts +336 -3
- package/src/http/factory.ts +7 -0
- package/src/index.ts +6 -0
- package/src/lifecycle/context.ts +75 -0
- package/src/lifecycle/interfaces.ts +3 -0
- package/src/microservices/client-proxy.ts +7 -0
- package/src/microservices/client-tcp.ts +127 -0
- package/src/microservices/decorators.ts +19 -0
- package/src/microservices/index.ts +6 -0
- package/src/microservices/interfaces.ts +11 -0
- package/src/microservices/microservice.ts +114 -0
- package/src/microservices/server-tcp.ts +210 -0
- package/src/schedule/cron.matcher.ts +45 -0
- package/src/schedule/decorators.ts +28 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/schedule.module.ts +13 -0
- package/src/security/cors.middleware.ts +50 -0
- package/src/security/hashing.service.ts +12 -0
- package/src/security/helmet.middleware.ts +45 -0
- package/src/security/index.ts +3 -0
- package/src/websockets/decorators.ts +49 -0
- package/src/websockets/gateway.ts +11 -0
- package/src/websockets/index.ts +2 -0
- package/tests/config-event.test.ts +145 -0
- package/tests/microservices.test.ts +105 -0
- package/tests/rpc-ws-context.test.ts +135 -0
- package/tests/schedule.test.ts +64 -0
- package/tests/security.test.ts +89 -0
- package/tests/websockets.test.ts +125 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Controller,
|
|
5
|
+
Get,
|
|
6
|
+
CalyxFactory,
|
|
7
|
+
HashingService,
|
|
8
|
+
} from '../src/index.ts';
|
|
9
|
+
|
|
10
|
+
@Controller('test-security')
|
|
11
|
+
class SecurityController {
|
|
12
|
+
constructor(private readonly hashing: HashingService) {}
|
|
13
|
+
|
|
14
|
+
@Get('hash')
|
|
15
|
+
async testHash() {
|
|
16
|
+
const password = 'my_secure_password';
|
|
17
|
+
const hash = await this.hashing.hash(password);
|
|
18
|
+
const valid = await this.hashing.verify(password, hash);
|
|
19
|
+
const invalid = await this.hashing.verify('wrong_password', hash);
|
|
20
|
+
return { valid, invalid };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Get('hello')
|
|
24
|
+
sayHello() {
|
|
25
|
+
return { hello: 'world' };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Module({
|
|
30
|
+
controllers: [SecurityController],
|
|
31
|
+
providers: [HashingService],
|
|
32
|
+
})
|
|
33
|
+
class SecurityTestApp {}
|
|
34
|
+
|
|
35
|
+
describe('Security Features (Hashing, CORS, Helmet)', () => {
|
|
36
|
+
let app: any;
|
|
37
|
+
let baseUrl: string;
|
|
38
|
+
const PORT = 3878;
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
app = await CalyxFactory.create(SecurityTestApp);
|
|
42
|
+
app.enableCors({ origin: 'http://example.com', credentials: true });
|
|
43
|
+
app.useHelmet({ contentSecurityPolicy: false }); // Disable CSP to make tests simpler
|
|
44
|
+
await app.listen(PORT);
|
|
45
|
+
baseUrl = `http://localhost:${PORT}`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
await app.close();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should hash and verify passwords using native Bun.password Zig bindings', async () => {
|
|
53
|
+
const res = await fetch(`${baseUrl}/test-security/hash`);
|
|
54
|
+
expect(res.status).toBe(200);
|
|
55
|
+
const body = await res.json();
|
|
56
|
+
expect(body.valid).toBe(true);
|
|
57
|
+
expect(body.invalid).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should apply CORS headers and respond to preflight OPTIONS request', async () => {
|
|
61
|
+
// 1. Regular GET request with Origin
|
|
62
|
+
const res = await fetch(`${baseUrl}/test-security/hello`, {
|
|
63
|
+
headers: { Origin: 'http://example.com' },
|
|
64
|
+
});
|
|
65
|
+
expect(res.status).toBe(200);
|
|
66
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('http://example.com');
|
|
67
|
+
expect(res.headers.get('access-control-allow-credentials')).toBe('true');
|
|
68
|
+
|
|
69
|
+
// 2. Preflight OPTIONS request
|
|
70
|
+
const preflight = await fetch(`${baseUrl}/test-security/hello`, {
|
|
71
|
+
method: 'OPTIONS',
|
|
72
|
+
headers: {
|
|
73
|
+
Origin: 'http://example.com',
|
|
74
|
+
'Access-Control-Request-Method': 'GET',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
expect(preflight.status).toBe(204);
|
|
78
|
+
expect(preflight.headers.get('access-control-allow-origin')).toBe('http://example.com');
|
|
79
|
+
expect(preflight.headers.get('access-control-allow-methods')).toContain('GET');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should set Helmet secure headers', async () => {
|
|
83
|
+
const res = await fetch(`${baseUrl}/test-security/hello`);
|
|
84
|
+
expect(res.status).toBe(200);
|
|
85
|
+
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
|
|
86
|
+
expect(res.headers.get('x-frame-options')).toBe('SAMEORIGIN');
|
|
87
|
+
expect(res.headers.get('referrer-policy')).toBe('no-referrer');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
CalyxFactory,
|
|
5
|
+
WebSocketGateway,
|
|
6
|
+
SubscribeMessage,
|
|
7
|
+
MessageBody,
|
|
8
|
+
ConnectedSocket,
|
|
9
|
+
} from '../src/index.ts';
|
|
10
|
+
|
|
11
|
+
let clientConnected = false;
|
|
12
|
+
let clientDisconnected = false;
|
|
13
|
+
let messageReceived: any = null;
|
|
14
|
+
|
|
15
|
+
// 1. Shared Port Gateway
|
|
16
|
+
@WebSocketGateway()
|
|
17
|
+
class SharedEventsGateway {
|
|
18
|
+
handleConnection(client: any) {
|
|
19
|
+
clientConnected = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handleDisconnect(client: any) {
|
|
23
|
+
clientDisconnected = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@SubscribeMessage('events')
|
|
27
|
+
onEvents(@MessageBody() data: any, @ConnectedSocket() client: any) {
|
|
28
|
+
messageReceived = data;
|
|
29
|
+
return { event: 'events', data: `echo: ${data}` };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Dedicated Port Gateway
|
|
34
|
+
let dedicatedConnected = false;
|
|
35
|
+
@WebSocketGateway(3912)
|
|
36
|
+
class DedicatedEventsGateway {
|
|
37
|
+
handleConnection(client: any) {
|
|
38
|
+
dedicatedConnected = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@SubscribeMessage('dedicated_event')
|
|
42
|
+
onDedicatedEvent(@MessageBody() data: any) {
|
|
43
|
+
return { event: 'dedicated_response', data: `dedicated echo: ${data}` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Module({
|
|
48
|
+
providers: [SharedEventsGateway, DedicatedEventsGateway],
|
|
49
|
+
})
|
|
50
|
+
class TestApp {}
|
|
51
|
+
|
|
52
|
+
describe('WebSocket Gateways (Bun.serve, SubscribeMessage, MessageBody, ConnectedSocket)', () => {
|
|
53
|
+
let app: any;
|
|
54
|
+
const PORT = 3889;
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
app = await CalyxFactory.create(TestApp);
|
|
58
|
+
await app.listen(PORT);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
afterAll(async () => {
|
|
62
|
+
await app.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('should connect and handle message exchange on shared port', async () => {
|
|
66
|
+
clientConnected = false;
|
|
67
|
+
clientDisconnected = false;
|
|
68
|
+
messageReceived = null;
|
|
69
|
+
|
|
70
|
+
const ws = new WebSocket(`ws://localhost:${PORT}`);
|
|
71
|
+
|
|
72
|
+
// Wait for connection to open
|
|
73
|
+
await new Promise<void>((resolve, reject) => {
|
|
74
|
+
ws.onopen = () => resolve();
|
|
75
|
+
ws.onerror = (err) => reject(err);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(clientConnected).toBe(true);
|
|
79
|
+
|
|
80
|
+
// Send a message
|
|
81
|
+
ws.send(JSON.stringify({ event: 'events', data: 'hello calyx' }));
|
|
82
|
+
|
|
83
|
+
// Wait for response message
|
|
84
|
+
const response = await new Promise<any>((resolve) => {
|
|
85
|
+
ws.onmessage = (event) => {
|
|
86
|
+
resolve(JSON.parse(event.data));
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(messageReceived).toBe('hello calyx');
|
|
91
|
+
expect(response).toEqual({ event: 'events', data: 'echo: hello calyx' });
|
|
92
|
+
|
|
93
|
+
// Close connection
|
|
94
|
+
ws.close();
|
|
95
|
+
|
|
96
|
+
// Wait a tick for disconnect event
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
98
|
+
expect(clientDisconnected).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('should connect and handle message exchange on dedicated port', async () => {
|
|
102
|
+
dedicatedConnected = false;
|
|
103
|
+
|
|
104
|
+
const ws = new WebSocket('ws://localhost:3912');
|
|
105
|
+
|
|
106
|
+
await new Promise<void>((resolve, reject) => {
|
|
107
|
+
ws.onopen = () => resolve();
|
|
108
|
+
ws.onerror = (err) => reject(err);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(dedicatedConnected).toBe(true);
|
|
112
|
+
|
|
113
|
+
ws.send(JSON.stringify({ event: 'dedicated_event', data: 'secure channel' }));
|
|
114
|
+
|
|
115
|
+
const response = await new Promise<any>((resolve) => {
|
|
116
|
+
ws.onmessage = (event) => {
|
|
117
|
+
resolve(JSON.parse(event.data));
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(response).toEqual({ event: 'dedicated_response', data: 'dedicated echo: secure channel' });
|
|
122
|
+
|
|
123
|
+
ws.close();
|
|
124
|
+
});
|
|
125
|
+
});
|