@stonyx/sockets 0.1.1-beta.8 → 0.1.1-beta.81
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 +39 -5
- package/config/environment.js +3 -1
- package/dist/client.d.ts +47 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +225 -0
- package/dist/client.js.map +1 -0
- package/dist/encryption.d.ts +5 -0
- package/dist/encryption.d.ts.map +1 -0
- package/dist/encryption.js +28 -0
- package/dist/encryption.js.map +1 -0
- package/dist/handler.d.ts +6 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +4 -0
- package/dist/handler.js.map +1 -0
- package/dist/main.d.ts +10 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +19 -0
- package/dist/main.js.map +1 -0
- package/dist/server.d.ts +49 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +191 -0
- package/dist/server.js.map +1 -0
- package/package.json +30 -11
- package/src/client.js +0 -197
- package/src/encryption.js +0 -31
- package/src/handler.js +0 -3
- package/src/main.js +0 -19
- package/src/server.js +0 -186
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-sockets/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/sockets)
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
|
|
1
5
|
# @stonyx/sockets
|
|
2
6
|
|
|
3
7
|
WebSocket server and client module for the [Stonyx framework](https://github.com/abofs/stonyx), providing plug-and-play handler discovery, built-in authentication enforcement, AES-256-GCM encryption, and automatic heartbeat keep-alive.
|
|
@@ -11,6 +15,19 @@ WebSocket server and client module for the [Stonyx framework](https://github.com
|
|
|
11
15
|
* **Encryption by default:** AES-256-GCM with per-session keys, zero external dependencies.
|
|
12
16
|
* **Singleton pattern:** Matches the conventions of `@stonyx/rest-server` and `stonyx-orm`.
|
|
13
17
|
|
|
18
|
+
## Samples
|
|
19
|
+
|
|
20
|
+
The handlers in [`test/sample/`](https://github.com/abofs/stonyx-sockets/tree/dev/test/sample) are the canonical usage reference for this module; the integration tests run against them. They are not included in the npm package. They are TypeScript and type-check against the published `Handler` type, which declares `server` and `client` as function-valued properties, so the TypeScript samples assign the hooks as `function` properties rather than methods. The JavaScript examples below use methods, which work the same at runtime.
|
|
21
|
+
|
|
22
|
+
Before deploying, read the [security notes](https://github.com/abofs/stonyx-sockets/blob/dev/docs/encryption.md#security-notes): `authKey` falls back to the literal `'AUTH_KEY'` if unset, any `encryption` value other than `'true'`/`true` runs in plaintext, and the module provides no transport security unless you use `wss://`.
|
|
23
|
+
|
|
24
|
+
## Exports
|
|
25
|
+
|
|
26
|
+
* `@stonyx/sockets`: named `SocketServer`, `SocketClient` and `Handler`; the default export is the `Sockets` class Stonyx uses for auto-init
|
|
27
|
+
* `@stonyx/sockets/server`: `SocketServer`
|
|
28
|
+
* `@stonyx/sockets/client`: `SocketClient`
|
|
29
|
+
* `@stonyx/sockets/handler`: `Handler`
|
|
30
|
+
|
|
14
31
|
## Installation
|
|
15
32
|
|
|
16
33
|
```bash
|
|
@@ -34,21 +51,31 @@ Each handler extends `Handler` and defines a `server()` method, a `client()` met
|
|
|
34
51
|
|
|
35
52
|
```javascript
|
|
36
53
|
// socket-handlers/auth.js
|
|
54
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
37
55
|
import { Handler } from '@stonyx/sockets';
|
|
38
56
|
import config from 'stonyx/config';
|
|
39
57
|
|
|
58
|
+
// Constant-time compare; timingSafeEqual throws on unequal lengths, so check length first.
|
|
59
|
+
function authKeyMatches(given) {
|
|
60
|
+
const { authKey } = config.sockets;
|
|
61
|
+
if (typeof authKey !== 'string' || authKey === '' || typeof given !== 'string') return false;
|
|
62
|
+
|
|
63
|
+
const a = Buffer.from(given), b = Buffer.from(authKey);
|
|
64
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
export default class AuthHandler extends Handler {
|
|
41
68
|
static skipAuth = true; // auth handler must work before authentication
|
|
42
69
|
|
|
43
70
|
server(data, client) {
|
|
44
|
-
if (data
|
|
71
|
+
if (!authKeyMatches(data?.authKey)) return client.close();
|
|
45
72
|
|
|
46
73
|
this._serverRef.clientMap.set(client.id, client);
|
|
47
74
|
return 'success';
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
client(response) {
|
|
51
|
-
if (response !== 'success') this.client.promise.reject(response);
|
|
78
|
+
if (response !== 'success') return this.client.promise.reject(response);
|
|
52
79
|
this.client.promise.resolve();
|
|
53
80
|
}
|
|
54
81
|
}
|
|
@@ -202,10 +229,14 @@ Configuration is read from `stonyx/config` under `sockets`:
|
|
|
202
229
|
|--------|---------|---------|-------------|
|
|
203
230
|
| `port` | `SOCKET_PORT` | `2667` | WebSocket server port |
|
|
204
231
|
| `address` | `SOCKET_ADDRESS` | `ws://localhost:2667` | Client connection address |
|
|
205
|
-
| `authKey` | `SOCKET_AUTH_KEY` | `'AUTH_KEY'` | Shared authentication key |
|
|
232
|
+
| `authKey` | `SOCKET_AUTH_KEY` | `'AUTH_KEY'` | Shared authentication key; set it in production (see the [security notes](https://github.com/abofs/stonyx-sockets/blob/dev/docs/encryption.md#security-notes)) |
|
|
206
233
|
| `heartBeatInterval` | `SOCKET_HEARTBEAT_INTERVAL` | `30000` | Heartbeat interval in ms |
|
|
207
234
|
| `handlerDir` | `SOCKET_HANDLER_DIR` | `'./socket-handlers'` | Handler directory path |
|
|
208
235
|
| `encryption` | `SOCKET_ENCRYPTION` | `'true'` | Enable AES-256-GCM encryption |
|
|
236
|
+
| `authData` | — | `{}` | Extra fields sent with `authKey` in the auth request |
|
|
237
|
+
| `reconnectBaseDelay` | `SOCKET_RECONNECT_BASE_DELAY` | `1000` | First reconnect delay in ms (doubles per attempt) |
|
|
238
|
+
| `reconnectMaxDelay` | `SOCKET_RECONNECT_MAX_DELAY` | `60000` | Reconnect delay cap in ms |
|
|
239
|
+
| `maxReconnectAttempts` | `SOCKET_MAX_RECONNECT_ATTEMPTS` | `Infinity` | Attempts before `onReconnectFailed` |
|
|
209
240
|
|
|
210
241
|
## API Reference
|
|
211
242
|
|
|
@@ -216,7 +247,9 @@ Configuration is read from `stonyx/config` under `sockets`:
|
|
|
216
247
|
| `new SocketServer()` | Singleton constructor |
|
|
217
248
|
| `async init()` | Discover handlers, validate auth, start WebSocket server |
|
|
218
249
|
| `sendTo(clientId, request, data)` | Send to one client by ID |
|
|
250
|
+
| `sendToByMeta(key, value, request, data)` | Send to the first authenticated client whose `meta[key] === value`; returns `true`/`false` |
|
|
219
251
|
| `broadcast(request, data)` | Send to all authenticated clients |
|
|
252
|
+
| `onClientDisconnect` | Optional `(client, code, reason)` callback |
|
|
220
253
|
| `clientMap` | `Map<id, client>` of connected clients |
|
|
221
254
|
| `close()` | Terminate all connections and stop the server |
|
|
222
255
|
| `reset()` | Close + clear all state (for testing) |
|
|
@@ -228,8 +261,9 @@ Configuration is read from `stonyx/config` under `sockets`:
|
|
|
228
261
|
| `new SocketClient()` | Singleton constructor |
|
|
229
262
|
| `async init()` | Discover handlers, connect, authenticate |
|
|
230
263
|
| `send(payload)` | Send a message to the server |
|
|
231
|
-
| `close()` |
|
|
232
|
-
| `reconnect()` | Reconnect (
|
|
264
|
+
| `close()` | Forcefully tear down the socket (remove listeners, terminate, null) |
|
|
265
|
+
| `reconnect()` | Reconnect with exponential backoff, up to `maxReconnectAttempts` (unlimited by default) |
|
|
266
|
+
| `onDisconnect`, `onReconnecting`, `onReconnected`, `onReconnectFailed` | Optional lifecycle callbacks |
|
|
233
267
|
| `reset()` | Close + clear all state (for testing) |
|
|
234
268
|
|
|
235
269
|
### Handler
|
package/config/environment.js
CHANGED
|
@@ -13,7 +13,7 @@ const {
|
|
|
13
13
|
|
|
14
14
|
const port = SOCKET_PORT ?? 2667;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
const config = {
|
|
17
17
|
port,
|
|
18
18
|
address: SOCKET_ADDRESS ?? `ws://localhost:${port}`,
|
|
19
19
|
authKey: SOCKET_AUTH_KEY ?? 'AUTH_KEY',
|
|
@@ -28,3 +28,5 @@ export default {
|
|
|
28
28
|
reconnectMaxDelay: SOCKET_RECONNECT_MAX_DELAY ?? 60000,
|
|
29
29
|
maxReconnectAttempts: SOCKET_MAX_RECONNECT_ATTEMPTS ?? Infinity,
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
export default config;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
interface SocketMessage {
|
|
3
|
+
request: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
response?: unknown;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
}
|
|
8
|
+
interface HandlerInstance {
|
|
9
|
+
_clientRef?: SocketClient;
|
|
10
|
+
client: (response: unknown) => void;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export default class SocketClient {
|
|
14
|
+
static instance: SocketClient | null;
|
|
15
|
+
handlers: Record<string, HandlerInstance>;
|
|
16
|
+
reconnectCount: number;
|
|
17
|
+
_intentionalClose: boolean;
|
|
18
|
+
socket: WebSocket | null;
|
|
19
|
+
sessionKey: Buffer | null;
|
|
20
|
+
globalKey: Buffer | null;
|
|
21
|
+
encryptionEnabled: boolean;
|
|
22
|
+
_heartBeatTimer: ReturnType<typeof setTimeout> | null;
|
|
23
|
+
_heartBeatResponseTimer: ReturnType<typeof setTimeout> | null;
|
|
24
|
+
promise: {
|
|
25
|
+
resolve: () => void;
|
|
26
|
+
reject: (reason?: unknown) => void;
|
|
27
|
+
} | null;
|
|
28
|
+
onDisconnect: ((code: number, reason: string) => void) | null;
|
|
29
|
+
onReconnecting: ((attempt: number, delay: number) => void) | null;
|
|
30
|
+
onReconnected: (() => void) | null;
|
|
31
|
+
onReconnectFailed: (() => void) | null;
|
|
32
|
+
constructor();
|
|
33
|
+
init(): Promise<void>;
|
|
34
|
+
discoverHandlers(): Promise<void>;
|
|
35
|
+
connect(): Promise<void>;
|
|
36
|
+
onMessage(payload: Buffer | string): void;
|
|
37
|
+
send(payload: SocketMessage, useGlobalKey?: boolean): void;
|
|
38
|
+
heartBeat(): void;
|
|
39
|
+
nextHeartBeat(): void;
|
|
40
|
+
onClose(code?: number, reason?: string): void;
|
|
41
|
+
close(): void;
|
|
42
|
+
getReconnectDelay(): number;
|
|
43
|
+
reconnect(): Promise<void>;
|
|
44
|
+
reset(): void;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAQ/B,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,eAAe;IACvB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,MAAM,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAErC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAM;IAC/C,cAAc,SAAK;IACnB,iBAAiB,UAAS;IAC1B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAQ;IAChC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAChC,iBAAiB,UAAS;IAC1B,eAAe,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAQ;IAC7D,uBAAuB,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,IAAI,CAAQ;IACrE,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAC;QAAC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,GAAG,IAAI,CAAQ;IAEnF,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IACrE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;IACzE,aAAa,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IAC1C,iBAAiB,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;;IAOxC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwBrB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAgC9B,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IA2CzC,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,YAAY,UAAQ,GAAG,IAAI;IAYxD,SAAS,IAAI,IAAI;IAIjB,aAAa,IAAI,IAAI;IAcrB,OAAO,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAa7C,KAAK,IAAI,IAAI;IAab,iBAAiB,IAAI,MAAM;IAYrB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyBhC,KAAK,IAAI,IAAI;CAYd"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import log from 'stonyx/log';
|
|
4
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
+
import { kebabCaseToCamelCase } from '@stonyx/utils/string';
|
|
6
|
+
import { sleep } from '@stonyx/utils/promise';
|
|
7
|
+
import { encrypt, decrypt, deriveKey } from './encryption.js';
|
|
8
|
+
export default class SocketClient {
|
|
9
|
+
static instance;
|
|
10
|
+
handlers = {};
|
|
11
|
+
reconnectCount = 0;
|
|
12
|
+
_intentionalClose = false;
|
|
13
|
+
socket = null;
|
|
14
|
+
sessionKey = null;
|
|
15
|
+
globalKey = null;
|
|
16
|
+
encryptionEnabled = false;
|
|
17
|
+
_heartBeatTimer = null;
|
|
18
|
+
_heartBeatResponseTimer = null;
|
|
19
|
+
promise = null;
|
|
20
|
+
onDisconnect = null;
|
|
21
|
+
onReconnecting = null;
|
|
22
|
+
onReconnected = null;
|
|
23
|
+
onReconnectFailed = null;
|
|
24
|
+
constructor() {
|
|
25
|
+
if (SocketClient.instance)
|
|
26
|
+
return SocketClient.instance;
|
|
27
|
+
SocketClient.instance = this;
|
|
28
|
+
}
|
|
29
|
+
async init() {
|
|
30
|
+
await this.discoverHandlers();
|
|
31
|
+
const { encryption, authKey } = config.sockets;
|
|
32
|
+
if (encryption === undefined) {
|
|
33
|
+
throw new Error('SocketClient: config.sockets.encryption is undefined — the @stonyx/sockets module ' +
|
|
34
|
+
'config was not merged. Refusing to connect (would send cleartext). Ensure Stonyx has ' +
|
|
35
|
+
'finished loading modules before constructing a SocketClient.');
|
|
36
|
+
}
|
|
37
|
+
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
38
|
+
if (this.encryptionEnabled) {
|
|
39
|
+
this.globalKey = deriveKey(authKey);
|
|
40
|
+
}
|
|
41
|
+
if (process.env.NODE_ENV === 'test')
|
|
42
|
+
return;
|
|
43
|
+
return this.connect();
|
|
44
|
+
}
|
|
45
|
+
async discoverHandlers() {
|
|
46
|
+
const { handlerDir } = config.sockets;
|
|
47
|
+
if (handlerDir === undefined) {
|
|
48
|
+
throw new Error('SocketClient: config.sockets.handlerDir is undefined — the @stonyx/sockets module ' +
|
|
49
|
+
'config was not merged. Ensure Stonyx has finished loading modules before constructing a SocketClient.');
|
|
50
|
+
}
|
|
51
|
+
await forEachFileImport(handlerDir, (HandlerClassUntyped, { name }) => {
|
|
52
|
+
const HandlerClass = HandlerClassUntyped;
|
|
53
|
+
const instance = new HandlerClass();
|
|
54
|
+
if (typeof instance.client === 'function') {
|
|
55
|
+
instance._clientRef = this;
|
|
56
|
+
this.handlers[name] = instance;
|
|
57
|
+
}
|
|
58
|
+
}, { ignoreAccessFailure: true });
|
|
59
|
+
}
|
|
60
|
+
async connect() {
|
|
61
|
+
if (this.socket) {
|
|
62
|
+
this.socket.removeAllListeners();
|
|
63
|
+
this.socket.on('error', () => { });
|
|
64
|
+
this.socket.terminate();
|
|
65
|
+
this.socket = null;
|
|
66
|
+
}
|
|
67
|
+
if (this.sessionKey)
|
|
68
|
+
log.socket('Clearing stale sessionKey');
|
|
69
|
+
this.sessionKey = null;
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const { address, authKey, authData } = config.sockets;
|
|
72
|
+
this.promise = { resolve, reject };
|
|
73
|
+
log.socket(`Connecting to remote server: ${address}`);
|
|
74
|
+
const socket = new WebSocket(address);
|
|
75
|
+
this.socket = socket;
|
|
76
|
+
socket.on('message', (data) => this.onMessage(data));
|
|
77
|
+
socket.on('close', (code, reason) => this.onClose(code, reason.toString()));
|
|
78
|
+
socket.on('error', () => {
|
|
79
|
+
log.socket(`Error connecting to socket server`);
|
|
80
|
+
reject(new Error('Error connecting to socket server'));
|
|
81
|
+
});
|
|
82
|
+
socket.on('open', () => {
|
|
83
|
+
this._intentionalClose = false;
|
|
84
|
+
this.reconnectCount = 0;
|
|
85
|
+
this.send({ request: 'auth', data: { authKey, ...authData } }, true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
onMessage(payload) {
|
|
90
|
+
try {
|
|
91
|
+
let parsed;
|
|
92
|
+
if (this.encryptionEnabled) {
|
|
93
|
+
const key = this.sessionKey || this.globalKey;
|
|
94
|
+
if (!key)
|
|
95
|
+
throw new Error('Encryption enabled but no key available');
|
|
96
|
+
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
97
|
+
parsed = JSON.parse(decrypt(raw, key));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const raw = Buffer.isBuffer(payload) ? payload.toString() : payload;
|
|
101
|
+
parsed = JSON.parse(raw);
|
|
102
|
+
}
|
|
103
|
+
const { request, response, sessionKey } = parsed;
|
|
104
|
+
if (request === 'auth') {
|
|
105
|
+
if (sessionKey && this.encryptionEnabled) {
|
|
106
|
+
this.sessionKey = Buffer.from(sessionKey, 'base64');
|
|
107
|
+
}
|
|
108
|
+
this.nextHeartBeat();
|
|
109
|
+
this.promise?.resolve();
|
|
110
|
+
this.promise = null;
|
|
111
|
+
}
|
|
112
|
+
if (request === 'heartBeat') {
|
|
113
|
+
this.nextHeartBeat();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const handler = this.handlers[request] ?? this.handlers[kebabCaseToCamelCase(request)];
|
|
117
|
+
if (!handler) {
|
|
118
|
+
log.socket(`Call to invalid handler: ${request}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
handler.client.call({ ...handler, client: this }, response);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
log.socket(`Invalid payload received from remote server`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
send(payload, useGlobalKey = false) {
|
|
128
|
+
if (!this.socket)
|
|
129
|
+
throw new Error('Socket is not connected');
|
|
130
|
+
if (this.encryptionEnabled) {
|
|
131
|
+
const key = useGlobalKey ? this.globalKey : this.sessionKey;
|
|
132
|
+
if (!key)
|
|
133
|
+
throw new Error('Encryption enabled but no key available');
|
|
134
|
+
const data = encrypt(JSON.stringify(payload), key);
|
|
135
|
+
this.socket.send(data);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
this.socket.send(JSON.stringify(payload));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
heartBeat() {
|
|
142
|
+
this.send({ request: 'heartBeat' });
|
|
143
|
+
}
|
|
144
|
+
nextHeartBeat() {
|
|
145
|
+
if (this._heartBeatResponseTimer)
|
|
146
|
+
clearTimeout(this._heartBeatResponseTimer);
|
|
147
|
+
this._heartBeatResponseTimer = null;
|
|
148
|
+
const { heartBeatInterval } = config.sockets;
|
|
149
|
+
this._heartBeatTimer = setTimeout(() => {
|
|
150
|
+
this.heartBeat();
|
|
151
|
+
this._heartBeatResponseTimer = setTimeout(() => {
|
|
152
|
+
log.socket('Heartbeat response timeout — closing stale connection');
|
|
153
|
+
if (this.socket)
|
|
154
|
+
this.socket.close();
|
|
155
|
+
}, heartBeatInterval);
|
|
156
|
+
}, heartBeatInterval);
|
|
157
|
+
}
|
|
158
|
+
// ws always provides code and reason; params are optional for direct calls and testing
|
|
159
|
+
onClose(code, reason) {
|
|
160
|
+
log.socket(`Disconnected from remote server (code: ${code ?? 'unknown'}, reason: ${reason || 'none'})`);
|
|
161
|
+
if (this._heartBeatTimer)
|
|
162
|
+
clearTimeout(this._heartBeatTimer);
|
|
163
|
+
if (this._heartBeatResponseTimer)
|
|
164
|
+
clearTimeout(this._heartBeatResponseTimer);
|
|
165
|
+
this._heartBeatResponseTimer = null;
|
|
166
|
+
this.onDisconnect?.(code ?? 1006, reason ?? '');
|
|
167
|
+
if (!this._intentionalClose) {
|
|
168
|
+
this.reconnect();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
close() {
|
|
172
|
+
this._intentionalClose = true;
|
|
173
|
+
if (this._heartBeatTimer)
|
|
174
|
+
clearTimeout(this._heartBeatTimer);
|
|
175
|
+
if (this._heartBeatResponseTimer)
|
|
176
|
+
clearTimeout(this._heartBeatResponseTimer);
|
|
177
|
+
this._heartBeatResponseTimer = null;
|
|
178
|
+
if (this.socket) {
|
|
179
|
+
this.socket.removeAllListeners();
|
|
180
|
+
this.socket.on('error', () => { });
|
|
181
|
+
this.socket.terminate();
|
|
182
|
+
this.socket = null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
getReconnectDelay() {
|
|
186
|
+
const { reconnectBaseDelay = 1000, reconnectMaxDelay = 60000, } = config.sockets;
|
|
187
|
+
const exponential = reconnectBaseDelay * Math.pow(2, this.reconnectCount - 1);
|
|
188
|
+
const capped = Math.min(exponential, reconnectMaxDelay);
|
|
189
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
190
|
+
return capped + jitter;
|
|
191
|
+
}
|
|
192
|
+
async reconnect() {
|
|
193
|
+
const { maxReconnectAttempts = Infinity } = config.sockets;
|
|
194
|
+
this.reconnectCount++;
|
|
195
|
+
if (this.reconnectCount > maxReconnectAttempts) {
|
|
196
|
+
log.socket('Max reconnect attempts exceeded');
|
|
197
|
+
this.onReconnectFailed?.();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const delay = this.getReconnectDelay();
|
|
201
|
+
this.onReconnecting?.(this.reconnectCount, delay);
|
|
202
|
+
log.socket(`Reconnecting (attempt ${this.reconnectCount}, delay ${delay}ms)`);
|
|
203
|
+
await sleep(delay / 1000);
|
|
204
|
+
try {
|
|
205
|
+
await this.connect();
|
|
206
|
+
this.onReconnected?.();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// onClose will fire and trigger the next reconnect attempt
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
reset() {
|
|
213
|
+
this.close();
|
|
214
|
+
this.handlers = {};
|
|
215
|
+
this.sessionKey = null;
|
|
216
|
+
this.reconnectCount = 0;
|
|
217
|
+
this._intentionalClose = false;
|
|
218
|
+
this.onDisconnect = null;
|
|
219
|
+
this.onReconnecting = null;
|
|
220
|
+
this.onReconnected = null;
|
|
221
|
+
this.onReconnectFailed = null;
|
|
222
|
+
SocketClient.instance = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function encrypt(data: string, key: Buffer): Buffer;
|
|
2
|
+
export declare function decrypt(buffer: Buffer | string, key: Buffer): string;
|
|
3
|
+
export declare function generateSessionKey(): Buffer;
|
|
4
|
+
export declare function deriveKey(authKey: string): Buffer;
|
|
5
|
+
//# sourceMappingURL=encryption.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAMA,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQpE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
3
|
+
const IV_LENGTH = 12;
|
|
4
|
+
const TAG_LENGTH = 16;
|
|
5
|
+
export function encrypt(data, key) {
|
|
6
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
7
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
8
|
+
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
|
9
|
+
const tag = cipher.getAuthTag();
|
|
10
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
11
|
+
}
|
|
12
|
+
export function decrypt(buffer, key) {
|
|
13
|
+
if (typeof buffer === 'string')
|
|
14
|
+
buffer = Buffer.from(buffer, 'base64');
|
|
15
|
+
const iv = buffer.subarray(0, IV_LENGTH);
|
|
16
|
+
const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
17
|
+
const encrypted = buffer.subarray(IV_LENGTH + TAG_LENGTH);
|
|
18
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
19
|
+
decipher.setAuthTag(tag);
|
|
20
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
|
21
|
+
}
|
|
22
|
+
export function generateSessionKey() {
|
|
23
|
+
return crypto.randomBytes(32);
|
|
24
|
+
}
|
|
25
|
+
export function deriveKey(authKey) {
|
|
26
|
+
return crypto.scryptSync(authKey, 'stonyx-sockets', 32);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=encryption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encryption.js","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,SAAS,GAAG,aAAa,CAAC;AAChC,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,GAAW;IAC/C,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC/E,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,MAAuB,EAAE,GAAW;IAC1D,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvE,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC;IAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,SAAS,GAAG,UAAU,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IAC7D,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IACzB,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;AAC1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,QAAQ,UAAS;IAEhB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxE,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;CAC9C"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC"}
|
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as SocketServer } from './server.js';
|
|
2
|
+
export { default as SocketClient } from './client.js';
|
|
3
|
+
export { default as Handler } from './handler.js';
|
|
4
|
+
export default class Sockets {
|
|
5
|
+
static instance: Sockets | null;
|
|
6
|
+
constructor();
|
|
7
|
+
init(): Promise<void>;
|
|
8
|
+
reset(): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC;;IAO1B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,KAAK,IAAI,IAAI;CAGd"}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { default as SocketServer } from './server.js';
|
|
2
|
+
export { default as SocketClient } from './client.js';
|
|
3
|
+
export { default as Handler } from './handler.js';
|
|
4
|
+
export default class Sockets {
|
|
5
|
+
static instance;
|
|
6
|
+
constructor() {
|
|
7
|
+
if (Sockets.instance)
|
|
8
|
+
return Sockets.instance;
|
|
9
|
+
Sockets.instance = this;
|
|
10
|
+
}
|
|
11
|
+
async init() {
|
|
12
|
+
// Handler discovery is deferred to SocketServer.init() / SocketClient.init()
|
|
13
|
+
// This entry point satisfies Stonyx module auto-initialization
|
|
14
|
+
}
|
|
15
|
+
reset() {
|
|
16
|
+
Sockets.instance = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,QAAQ,CAAiB;IAEhC;QACE,IAAI,OAAO,CAAC,QAAQ;YAAE,OAAO,OAAO,CAAC,QAAQ,CAAC;QAC9C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,6EAA6E;QAC7E,+DAA+D;IACjE,CAAC;IAED,KAAK;QACH,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC;CACF"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
interface SocketMessage {
|
|
3
|
+
request: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
response?: unknown;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ConnectedClient {
|
|
9
|
+
id: number;
|
|
10
|
+
ip: string;
|
|
11
|
+
__authenticated: boolean;
|
|
12
|
+
__sessionKey?: Buffer;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
14
|
+
send(payload: SocketMessage, keyOverride?: Buffer): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
terminate(): void;
|
|
17
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
18
|
+
}
|
|
19
|
+
interface HandlerInstance {
|
|
20
|
+
_serverRef?: SocketServer;
|
|
21
|
+
server: (data: unknown, client: ConnectedClient) => unknown | Promise<unknown>;
|
|
22
|
+
constructor: {
|
|
23
|
+
skipAuth: boolean;
|
|
24
|
+
};
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export default class SocketServer {
|
|
28
|
+
static instance: SocketServer | null;
|
|
29
|
+
clientMap: Map<number, ConnectedClient>;
|
|
30
|
+
handlers: Record<string, HandlerInstance>;
|
|
31
|
+
wss: WebSocketServer | null;
|
|
32
|
+
encryptionEnabled: boolean;
|
|
33
|
+
globalKey: Buffer | null;
|
|
34
|
+
onClientDisconnect: ((client: ConnectedClient, code: number, reason: string) => void) | null;
|
|
35
|
+
constructor();
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
discoverHandlers(): Promise<void>;
|
|
38
|
+
validateAuthHandler(): void;
|
|
39
|
+
onMessage(payload: Buffer | string, client: ConnectedClient): Promise<void>;
|
|
40
|
+
prepareSend(client: ConnectedClient, ws: WebSocket): void;
|
|
41
|
+
handleDisconnect(client: ConnectedClient, code?: number, reason?: string): void;
|
|
42
|
+
sendTo(clientId: number, request: string, response: unknown): void;
|
|
43
|
+
sendToByMeta(key: string, value: unknown, request: string, response: unknown): boolean;
|
|
44
|
+
broadcast(request: string, response: unknown): void;
|
|
45
|
+
close(): void;
|
|
46
|
+
reset(): void;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAOhD,UAAU,aAAa;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzD,KAAK,IAAI,IAAI,CAAC;IACd,SAAS,IAAI,IAAI,CAAC;IAClB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;CAChE;AAED,UAAU,eAAe;IACvB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,eAAe,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/E,WAAW,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAID,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,MAAM,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAErC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAa;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAM;IAC/C,GAAG,EAAE,eAAe,GAAG,IAAI,CAAQ;IACnC,iBAAiB,UAAS;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAQ;IAEhC,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,CAAQ;;IAO9F,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA6CrB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBvC,mBAAmB,IAAI,IAAI;IAMrB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA2DjF,WAAW,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI;IAiBzD,gBAAgB,CAAC,MAAM,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAO/E,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IAMlE,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO;IAUtF,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI;IAQnD,KAAK,IAAI,IAAI;IAUb,KAAK,IAAI,IAAI;CAOd"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import log from 'stonyx/log';
|
|
4
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
+
import { kebabCaseToCamelCase } from '@stonyx/utils/string';
|
|
6
|
+
import { encrypt, decrypt, deriveKey, generateSessionKey } from './encryption.js';
|
|
7
|
+
let clientId = 0;
|
|
8
|
+
export default class SocketServer {
|
|
9
|
+
static instance;
|
|
10
|
+
clientMap = new Map();
|
|
11
|
+
handlers = {};
|
|
12
|
+
wss = null;
|
|
13
|
+
encryptionEnabled = false;
|
|
14
|
+
globalKey = null;
|
|
15
|
+
onClientDisconnect = null;
|
|
16
|
+
constructor() {
|
|
17
|
+
if (SocketServer.instance)
|
|
18
|
+
return SocketServer.instance;
|
|
19
|
+
SocketServer.instance = this;
|
|
20
|
+
}
|
|
21
|
+
async init() {
|
|
22
|
+
// Self-register so log.socket works even when @stonyx/sockets is in the
|
|
23
|
+
// consumer's `dependencies` (stonyx loader only merges devDependencies).
|
|
24
|
+
const { logColor = 'white', logMethod = 'socket' } = config.sockets;
|
|
25
|
+
log.defineType(logMethod, logColor);
|
|
26
|
+
await this.discoverHandlers();
|
|
27
|
+
this.validateAuthHandler();
|
|
28
|
+
const { port, encryption, authKey } = config.sockets;
|
|
29
|
+
if (encryption === undefined) {
|
|
30
|
+
throw new Error('SocketServer: config.sockets.encryption is undefined — the @stonyx/sockets module ' +
|
|
31
|
+
'config was not merged. Refusing to start (would accept cleartext). Ensure Stonyx has ' +
|
|
32
|
+
'finished loading modules before constructing a SocketServer.');
|
|
33
|
+
}
|
|
34
|
+
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
35
|
+
if (this.encryptionEnabled) {
|
|
36
|
+
this.globalKey = deriveKey(authKey);
|
|
37
|
+
}
|
|
38
|
+
const wss = new WebSocketServer({ port });
|
|
39
|
+
this.wss = wss;
|
|
40
|
+
log.socket(`WebSocket server is listening on port ${port}`);
|
|
41
|
+
wss.on('connection', (ws, request) => {
|
|
42
|
+
const { remoteAddress } = request.socket;
|
|
43
|
+
log.socket(`[${remoteAddress}] Client connected`);
|
|
44
|
+
const client = ws;
|
|
45
|
+
client.id = ++clientId;
|
|
46
|
+
client.ip = remoteAddress || '';
|
|
47
|
+
client.__authenticated = false;
|
|
48
|
+
this.prepareSend(client, ws);
|
|
49
|
+
ws.on('message', (payload) => this.onMessage(payload, client));
|
|
50
|
+
ws.on('close', (code, reason) => this.handleDisconnect(client, code, reason.toString()));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async discoverHandlers() {
|
|
54
|
+
const { handlerDir } = config.sockets;
|
|
55
|
+
if (handlerDir === undefined) {
|
|
56
|
+
throw new Error('SocketServer: config.sockets.handlerDir is undefined — the @stonyx/sockets module ' +
|
|
57
|
+
'config was not merged. Ensure Stonyx has finished loading modules before constructing a SocketServer.');
|
|
58
|
+
}
|
|
59
|
+
await forEachFileImport(handlerDir, (HandlerClassUntyped, { name }) => {
|
|
60
|
+
const HandlerClass = HandlerClassUntyped;
|
|
61
|
+
const instance = new HandlerClass();
|
|
62
|
+
if (typeof instance.server === 'function') {
|
|
63
|
+
instance._serverRef = this;
|
|
64
|
+
this.handlers[name] = instance;
|
|
65
|
+
}
|
|
66
|
+
}, { ignoreAccessFailure: true });
|
|
67
|
+
}
|
|
68
|
+
validateAuthHandler() {
|
|
69
|
+
if (!this.handlers.auth) {
|
|
70
|
+
throw new Error('SocketServer requires an "auth" handler with a server() method');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async onMessage(payload, client) {
|
|
74
|
+
try {
|
|
75
|
+
let parsed;
|
|
76
|
+
if (this.encryptionEnabled) {
|
|
77
|
+
const key = client.__authenticated ? client.__sessionKey : this.globalKey;
|
|
78
|
+
if (!key)
|
|
79
|
+
throw new Error('Encryption enabled but no key available');
|
|
80
|
+
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
81
|
+
parsed = JSON.parse(decrypt(raw, key));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
parsed = JSON.parse(typeof payload === 'string' ? payload : payload.toString());
|
|
85
|
+
}
|
|
86
|
+
const { request, data } = parsed;
|
|
87
|
+
// Built-in heartbeat - no handler needed
|
|
88
|
+
if (request === 'heartBeat') {
|
|
89
|
+
if (client.__authenticated)
|
|
90
|
+
client.send({ request: 'heartBeat', response: true });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const handler = this.handlers[request] ?? this.handlers[kebabCaseToCamelCase(request)];
|
|
94
|
+
if (!handler) {
|
|
95
|
+
log.socket(`Invalid request received: ${request}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (request !== 'auth' && !handler.constructor.skipAuth && !client.__authenticated) {
|
|
99
|
+
log.socket(`Rejected unauthenticated request: ${request}`);
|
|
100
|
+
client.close();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const response = await handler.server(data, client);
|
|
104
|
+
if (response === undefined || response === null)
|
|
105
|
+
return;
|
|
106
|
+
if (request === 'auth' && response) {
|
|
107
|
+
client.__authenticated = true;
|
|
108
|
+
if (this.encryptionEnabled) {
|
|
109
|
+
const sessionKey = generateSessionKey();
|
|
110
|
+
client.__sessionKey = sessionKey;
|
|
111
|
+
if (!this.globalKey)
|
|
112
|
+
throw new Error('Encryption enabled but globalKey not initialized');
|
|
113
|
+
client.send({ request, response, sessionKey: sessionKey.toString('base64') }, this.globalKey);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
client.send({ request, response });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
client.send({ request, response });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
log.socket(`Invalid payload from client`);
|
|
124
|
+
if (config.debug)
|
|
125
|
+
console.error(error);
|
|
126
|
+
client.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
prepareSend(client, ws) {
|
|
130
|
+
const socketSend = ws.send.bind(ws);
|
|
131
|
+
const server = this;
|
|
132
|
+
client.send = (payload, keyOverride) => {
|
|
133
|
+
if (server.encryptionEnabled) {
|
|
134
|
+
const key = keyOverride || client.__sessionKey;
|
|
135
|
+
if (!key)
|
|
136
|
+
throw new Error('Encryption enabled but no key available');
|
|
137
|
+
const data = encrypt(JSON.stringify(payload), key);
|
|
138
|
+
socketSend(data);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
socketSend(JSON.stringify(payload));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ws always provides code and reason; params are optional for direct calls and testing
|
|
146
|
+
handleDisconnect(client, code, reason) {
|
|
147
|
+
const { ip } = client;
|
|
148
|
+
log.socket(`[${ip}] Client disconnected (code: ${code ?? 'unknown'}, reason: ${reason || 'none'})`);
|
|
149
|
+
this.clientMap.delete(client.id);
|
|
150
|
+
this.onClientDisconnect?.(client, code ?? 1006, reason ?? '');
|
|
151
|
+
}
|
|
152
|
+
sendTo(clientId, request, response) {
|
|
153
|
+
const client = this.clientMap.get(clientId);
|
|
154
|
+
if (!client)
|
|
155
|
+
return;
|
|
156
|
+
client.send({ request, response });
|
|
157
|
+
}
|
|
158
|
+
sendToByMeta(key, value, request, response) {
|
|
159
|
+
for (const [, client] of this.clientMap) {
|
|
160
|
+
if (client.meta?.[key] === value && client.__authenticated) {
|
|
161
|
+
client.send({ request, response });
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
broadcast(request, response) {
|
|
168
|
+
for (const [, client] of this.clientMap) {
|
|
169
|
+
if (client.__authenticated) {
|
|
170
|
+
client.send({ request, response });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
close() {
|
|
175
|
+
if (this.wss) {
|
|
176
|
+
for (const client of this.wss.clients) {
|
|
177
|
+
client.terminate();
|
|
178
|
+
}
|
|
179
|
+
this.wss.close();
|
|
180
|
+
this.wss = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
reset() {
|
|
184
|
+
this.close();
|
|
185
|
+
this.clientMap.clear();
|
|
186
|
+
this.handlers = {};
|
|
187
|
+
clientId = 0;
|
|
188
|
+
SocketServer.instance = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -4,21 +4,34 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.1-beta.
|
|
7
|
+
"version": "0.1.1-beta.81",
|
|
8
8
|
"description": "WebSocket server and client module for the Stonyx framework",
|
|
9
|
-
"main": "
|
|
9
|
+
"main": "dist/main.js",
|
|
10
|
+
"types": "dist/main.d.ts",
|
|
10
11
|
"type": "module",
|
|
11
12
|
"files": [
|
|
12
|
-
"
|
|
13
|
+
"dist",
|
|
13
14
|
"config",
|
|
14
15
|
"LICENSE.md",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
|
-
".":
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/main.d.ts",
|
|
21
|
+
"default": "./dist/main.js"
|
|
22
|
+
},
|
|
23
|
+
"./server": {
|
|
24
|
+
"types": "./dist/server.d.ts",
|
|
25
|
+
"default": "./dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/client.d.ts",
|
|
29
|
+
"default": "./dist/client.js"
|
|
30
|
+
},
|
|
31
|
+
"./handler": {
|
|
32
|
+
"types": "./dist/handler.d.ts",
|
|
33
|
+
"default": "./dist/handler.js"
|
|
34
|
+
}
|
|
22
35
|
},
|
|
23
36
|
"publishConfig": {
|
|
24
37
|
"access": "public",
|
|
@@ -38,15 +51,21 @@
|
|
|
38
51
|
},
|
|
39
52
|
"homepage": "https://github.com/abofs/stonyx-sockets#readme",
|
|
40
53
|
"dependencies": {
|
|
41
|
-
"stonyx": "0.2.3-beta.
|
|
54
|
+
"stonyx": "0.2.3-beta.106",
|
|
42
55
|
"ws": "^8.18.1"
|
|
43
56
|
},
|
|
44
57
|
"devDependencies": {
|
|
45
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
58
|
+
"@stonyx/utils": "0.2.3-beta.29",
|
|
59
|
+
"@types/node": "^25.5.2",
|
|
60
|
+
"@types/ws": "^8.18.1",
|
|
46
61
|
"qunit": "^2.24.1",
|
|
47
|
-
"sinon": "^21.0.0"
|
|
62
|
+
"sinon": "^21.0.0",
|
|
63
|
+
"tsx": "^4.21.0",
|
|
64
|
+
"typescript": "^5.8.3"
|
|
48
65
|
},
|
|
49
66
|
"scripts": {
|
|
50
|
-
"
|
|
67
|
+
"build": "tsc",
|
|
68
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
69
|
+
"test": "pnpm build && pnpm build:test && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
|
|
51
70
|
}
|
|
52
71
|
}
|
package/src/client.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from 'ws';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import log from 'stonyx/log';
|
|
4
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
-
import { sleep } from '@stonyx/utils/promise';
|
|
6
|
-
import { encrypt, decrypt, deriveKey } from './encryption.js';
|
|
7
|
-
|
|
8
|
-
export default class SocketClient {
|
|
9
|
-
handlers = {};
|
|
10
|
-
reconnectCount = 0;
|
|
11
|
-
_intentionalClose = false;
|
|
12
|
-
|
|
13
|
-
onDisconnect = null;
|
|
14
|
-
onReconnecting = null;
|
|
15
|
-
onReconnected = null;
|
|
16
|
-
onReconnectFailed = null;
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
if (SocketClient.instance) return SocketClient.instance;
|
|
20
|
-
SocketClient.instance = this;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async init() {
|
|
24
|
-
await this.discoverHandlers();
|
|
25
|
-
|
|
26
|
-
const { encryption, authKey } = config.sockets;
|
|
27
|
-
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
28
|
-
|
|
29
|
-
if (this.encryptionEnabled) {
|
|
30
|
-
this.globalKey = deriveKey(authKey);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return this.connect();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async discoverHandlers() {
|
|
37
|
-
const { handlerDir } = config.sockets;
|
|
38
|
-
|
|
39
|
-
await forEachFileImport(handlerDir, (HandlerClass, { name }) => {
|
|
40
|
-
const instance = new HandlerClass();
|
|
41
|
-
|
|
42
|
-
if (typeof instance.client === 'function') {
|
|
43
|
-
instance._clientRef = this;
|
|
44
|
-
this.handlers[name] = instance;
|
|
45
|
-
}
|
|
46
|
-
}, { ignoreAccessFailure: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async connect() {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const { address, authKey, authData } = config.sockets;
|
|
52
|
-
this.promise = { resolve, reject };
|
|
53
|
-
|
|
54
|
-
log.socket(`Connecting to remote server: ${address}`);
|
|
55
|
-
const socket = new WebSocket(address);
|
|
56
|
-
this.socket = socket;
|
|
57
|
-
|
|
58
|
-
socket.onmessage = this.onMessage.bind(this);
|
|
59
|
-
socket.onclose = this.onClose.bind(this);
|
|
60
|
-
socket.onerror = event => {
|
|
61
|
-
log.socket(`Error connecting to socket server`);
|
|
62
|
-
reject('Error connecting to socket server');
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
socket.onopen = () => {
|
|
66
|
-
this._intentionalClose = false;
|
|
67
|
-
this.reconnectCount = 0;
|
|
68
|
-
this.send({ request: 'auth', data: { authKey, ...authData } }, true);
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
onMessage({ data: payload }) {
|
|
74
|
-
try {
|
|
75
|
-
let parsed;
|
|
76
|
-
|
|
77
|
-
if (this.encryptionEnabled) {
|
|
78
|
-
const key = this.sessionKey || this.globalKey;
|
|
79
|
-
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
80
|
-
parsed = JSON.parse(decrypt(raw, key));
|
|
81
|
-
} else {
|
|
82
|
-
const raw = Buffer.isBuffer(payload) ? payload.toString() : payload;
|
|
83
|
-
parsed = JSON.parse(raw);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const { request, response, sessionKey } = parsed;
|
|
87
|
-
|
|
88
|
-
if (request === 'auth') {
|
|
89
|
-
if (sessionKey && this.encryptionEnabled) {
|
|
90
|
-
this.sessionKey = Buffer.from(sessionKey, 'base64');
|
|
91
|
-
}
|
|
92
|
-
this.nextHeartBeat();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (request === 'heartBeat') {
|
|
96
|
-
this.nextHeartBeat();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const handler = this.handlers[request];
|
|
101
|
-
|
|
102
|
-
if (!handler) {
|
|
103
|
-
log.socket(`Call to invalid handler: ${request}`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
handler.client.call({ client: this, ...handler }, response);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
log.socket(`Invalid payload received from remote server`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
send(payload, useGlobalKey = false) {
|
|
114
|
-
if (this.encryptionEnabled) {
|
|
115
|
-
const key = useGlobalKey ? this.globalKey : this.sessionKey;
|
|
116
|
-
const data = encrypt(JSON.stringify(payload), key);
|
|
117
|
-
this.socket.send(data);
|
|
118
|
-
} else {
|
|
119
|
-
this.socket.send(JSON.stringify(payload));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
heartBeat() {
|
|
124
|
-
this.send({ request: 'heartBeat' });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
nextHeartBeat() {
|
|
128
|
-
this._heartBeatTimer = setTimeout(() => this.heartBeat(), config.sockets.heartBeatInterval);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
onClose() {
|
|
132
|
-
log.socket('Disconnected from remote server');
|
|
133
|
-
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
134
|
-
|
|
135
|
-
this.onDisconnect?.();
|
|
136
|
-
|
|
137
|
-
if (!this._intentionalClose) {
|
|
138
|
-
this.reconnect();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
close() {
|
|
143
|
-
this._intentionalClose = true;
|
|
144
|
-
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
145
|
-
if (this.socket) this.socket.close();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
getReconnectDelay() {
|
|
149
|
-
const {
|
|
150
|
-
reconnectBaseDelay = 1000,
|
|
151
|
-
reconnectMaxDelay = 60000,
|
|
152
|
-
} = config.sockets;
|
|
153
|
-
|
|
154
|
-
const exponential = reconnectBaseDelay * Math.pow(2, this.reconnectCount - 1);
|
|
155
|
-
const capped = Math.min(exponential, reconnectMaxDelay);
|
|
156
|
-
const jitter = Math.floor(Math.random() * 1000);
|
|
157
|
-
return capped + jitter;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async reconnect() {
|
|
161
|
-
const { maxReconnectAttempts = Infinity } = config.sockets;
|
|
162
|
-
|
|
163
|
-
this.reconnectCount++;
|
|
164
|
-
|
|
165
|
-
if (this.reconnectCount > maxReconnectAttempts) {
|
|
166
|
-
log.socket('Max reconnect attempts exceeded');
|
|
167
|
-
this.onReconnectFailed?.();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const delay = this.getReconnectDelay();
|
|
172
|
-
this.onReconnecting?.(this.reconnectCount, delay);
|
|
173
|
-
log.socket(`Reconnecting (attempt ${this.reconnectCount}, delay ${delay}ms)`);
|
|
174
|
-
|
|
175
|
-
await sleep(delay / 1000);
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await this.connect();
|
|
179
|
-
this.onReconnected?.();
|
|
180
|
-
} catch {
|
|
181
|
-
// onClose will fire and trigger the next reconnect attempt
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
reset() {
|
|
186
|
-
this.close();
|
|
187
|
-
this.handlers = {};
|
|
188
|
-
this.sessionKey = null;
|
|
189
|
-
this.reconnectCount = 0;
|
|
190
|
-
this._intentionalClose = false;
|
|
191
|
-
this.onDisconnect = null;
|
|
192
|
-
this.onReconnecting = null;
|
|
193
|
-
this.onReconnected = null;
|
|
194
|
-
this.onReconnectFailed = null;
|
|
195
|
-
SocketClient.instance = null;
|
|
196
|
-
}
|
|
197
|
-
}
|
package/src/encryption.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
-
const IV_LENGTH = 12;
|
|
5
|
-
const TAG_LENGTH = 16;
|
|
6
|
-
|
|
7
|
-
export function encrypt(data, key) {
|
|
8
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
9
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
10
|
-
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
|
11
|
-
const tag = cipher.getAuthTag();
|
|
12
|
-
return Buffer.concat([iv, tag, encrypted]);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function decrypt(buffer, key) {
|
|
16
|
-
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'base64');
|
|
17
|
-
const iv = buffer.subarray(0, IV_LENGTH);
|
|
18
|
-
const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
19
|
-
const encrypted = buffer.subarray(IV_LENGTH + TAG_LENGTH);
|
|
20
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
21
|
-
decipher.setAuthTag(tag);
|
|
22
|
-
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function generateSessionKey() {
|
|
26
|
-
return crypto.randomBytes(32);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function deriveKey(authKey) {
|
|
30
|
-
return crypto.scryptSync(authKey, 'stonyx-sockets', 32);
|
|
31
|
-
}
|
package/src/handler.js
DELETED
package/src/main.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export { default as SocketServer } from './server.js';
|
|
2
|
-
export { default as SocketClient } from './client.js';
|
|
3
|
-
export { default as Handler } from './handler.js';
|
|
4
|
-
|
|
5
|
-
export default class Sockets {
|
|
6
|
-
constructor() {
|
|
7
|
-
if (Sockets.instance) return Sockets.instance;
|
|
8
|
-
Sockets.instance = this;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async init() {
|
|
12
|
-
// Handler discovery is deferred to SocketServer.init() / SocketClient.init()
|
|
13
|
-
// This entry point satisfies Stonyx module auto-initialization
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
reset() {
|
|
17
|
-
Sockets.instance = null;
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer } from 'ws';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import log from 'stonyx/log';
|
|
4
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
-
import { encrypt, decrypt, deriveKey, generateSessionKey } from './encryption.js';
|
|
6
|
-
|
|
7
|
-
let clientId = 0;
|
|
8
|
-
|
|
9
|
-
export default class SocketServer {
|
|
10
|
-
clientMap = new Map();
|
|
11
|
-
handlers = {};
|
|
12
|
-
|
|
13
|
-
constructor() {
|
|
14
|
-
if (SocketServer.instance) return SocketServer.instance;
|
|
15
|
-
SocketServer.instance = this;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async init() {
|
|
19
|
-
await this.discoverHandlers();
|
|
20
|
-
this.validateAuthHandler();
|
|
21
|
-
|
|
22
|
-
const { port, encryption, authKey } = config.sockets;
|
|
23
|
-
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
24
|
-
|
|
25
|
-
if (this.encryptionEnabled) {
|
|
26
|
-
this.globalKey = deriveKey(authKey);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const wss = new WebSocketServer({ port });
|
|
30
|
-
this.wss = wss;
|
|
31
|
-
|
|
32
|
-
log.socket(`WebSocket server is listening on port ${port}`);
|
|
33
|
-
|
|
34
|
-
wss.on('connection', (client, request) => {
|
|
35
|
-
const { remoteAddress } = request.socket;
|
|
36
|
-
log.socket(`[${remoteAddress}] Client connected`);
|
|
37
|
-
client.id = ++clientId;
|
|
38
|
-
client.ip = remoteAddress;
|
|
39
|
-
client.__authenticated = false;
|
|
40
|
-
this.prepareSend(client);
|
|
41
|
-
|
|
42
|
-
client.on('message', payload => this.onMessage(payload, client));
|
|
43
|
-
client.on('close', () => this.handleDisconnect(client));
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async discoverHandlers() {
|
|
48
|
-
const { handlerDir } = config.sockets;
|
|
49
|
-
|
|
50
|
-
await forEachFileImport(handlerDir, (HandlerClass, { name }) => {
|
|
51
|
-
const instance = new HandlerClass();
|
|
52
|
-
|
|
53
|
-
if (typeof instance.server === 'function') {
|
|
54
|
-
instance._serverRef = this;
|
|
55
|
-
this.handlers[name] = instance;
|
|
56
|
-
}
|
|
57
|
-
}, { ignoreAccessFailure: true });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
validateAuthHandler() {
|
|
61
|
-
if (!this.handlers.auth) {
|
|
62
|
-
throw new Error('SocketServer requires an "auth" handler with a server() method');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async onMessage(payload, client) {
|
|
67
|
-
try {
|
|
68
|
-
let parsed;
|
|
69
|
-
|
|
70
|
-
if (this.encryptionEnabled) {
|
|
71
|
-
const key = client.__authenticated ? client.__sessionKey : this.globalKey;
|
|
72
|
-
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
73
|
-
parsed = JSON.parse(decrypt(raw, key));
|
|
74
|
-
} else {
|
|
75
|
-
parsed = JSON.parse(payload);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const { request, data } = parsed;
|
|
79
|
-
|
|
80
|
-
// Built-in heartbeat — no handler needed
|
|
81
|
-
if (request === 'heartBeat') {
|
|
82
|
-
if (client.__authenticated) client.send({ request: 'heartBeat', response: true });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const handler = this.handlers[request];
|
|
87
|
-
|
|
88
|
-
if (!handler) {
|
|
89
|
-
log.socket(`Invalid request received: ${request}`);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (request !== 'auth' && !handler.constructor.skipAuth && !client.__authenticated) {
|
|
94
|
-
log.socket(`Rejected unauthenticated request: ${request}`);
|
|
95
|
-
client.close();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const response = await handler.server(data, client);
|
|
100
|
-
if (response === undefined || response === null) return;
|
|
101
|
-
|
|
102
|
-
if (request === 'auth' && response) {
|
|
103
|
-
client.__authenticated = true;
|
|
104
|
-
|
|
105
|
-
if (this.encryptionEnabled) {
|
|
106
|
-
const sessionKey = generateSessionKey();
|
|
107
|
-
client.__sessionKey = sessionKey;
|
|
108
|
-
client.send({ request, response, sessionKey: sessionKey.toString('base64') }, this.globalKey);
|
|
109
|
-
} else {
|
|
110
|
-
client.send({ request, response });
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
client.send({ request, response });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
log.socket(`Invalid payload from client`);
|
|
118
|
-
if (config.debug) console.error(error);
|
|
119
|
-
client.close();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
prepareSend(client) {
|
|
124
|
-
const { send: socketSend } = client;
|
|
125
|
-
const server = this;
|
|
126
|
-
|
|
127
|
-
client.send = (payload, keyOverride) => {
|
|
128
|
-
if (server.encryptionEnabled) {
|
|
129
|
-
const key = keyOverride || client.__sessionKey;
|
|
130
|
-
const data = encrypt(JSON.stringify(payload), key);
|
|
131
|
-
socketSend.bind(client)(data);
|
|
132
|
-
} else {
|
|
133
|
-
socketSend.bind(client)(JSON.stringify(payload));
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
handleDisconnect(client) {
|
|
139
|
-
const { ip } = client;
|
|
140
|
-
log.socket(`[${ip}] Client disconnected`);
|
|
141
|
-
this.clientMap.delete(client.id);
|
|
142
|
-
this.onClientDisconnect?.(client);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
sendTo(clientId, request, response) {
|
|
146
|
-
const client = this.clientMap.get(clientId);
|
|
147
|
-
if (!client) return;
|
|
148
|
-
client.send({ request, response });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
sendToByMeta(key, value, request, response) {
|
|
152
|
-
for (const [, client] of this.clientMap) {
|
|
153
|
-
if (client.meta?.[key] === value && client.__authenticated) {
|
|
154
|
-
client.send({ request, response });
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
broadcast(request, response) {
|
|
162
|
-
for (const [, client] of this.clientMap) {
|
|
163
|
-
if (client.__authenticated) {
|
|
164
|
-
client.send({ request, response });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
close() {
|
|
170
|
-
if (this.wss) {
|
|
171
|
-
for (const client of this.wss.clients) {
|
|
172
|
-
client.terminate();
|
|
173
|
-
}
|
|
174
|
-
this.wss.close();
|
|
175
|
-
this.wss = null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
reset() {
|
|
180
|
-
this.close();
|
|
181
|
-
this.clientMap.clear();
|
|
182
|
-
this.handlers = {};
|
|
183
|
-
clientId = 0;
|
|
184
|
-
SocketServer.instance = null;
|
|
185
|
-
}
|
|
186
|
-
}
|