@stonyx/sockets 0.1.1-beta.7 → 0.1.1-beta.71
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 +5 -1
- 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.
|
|
@@ -228,7 +232,7 @@ Configuration is read from `stonyx/config` under `sockets`:
|
|
|
228
232
|
| `new SocketClient()` | Singleton constructor |
|
|
229
233
|
| `async init()` | Discover handlers, connect, authenticate |
|
|
230
234
|
| `send(payload)` | Send a message to the server |
|
|
231
|
-
| `close()` |
|
|
235
|
+
| `close()` | Forcefully tear down the socket (remove listeners, terminate, null) |
|
|
232
236
|
| `reconnect()` | Reconnect (max 5 retries) |
|
|
233
237
|
| `reset()` | Close + clear all state (for testing) |
|
|
234
238
|
|
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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,GAAG,MAAM,YAAY,CAAC;AAC7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAe9D,MAAM,CAAC,OAAO,OAAO,YAAY;IAC/B,MAAM,CAAC,QAAQ,CAAsB;IAErC,QAAQ,GAAoC,EAAE,CAAC;IAC/C,cAAc,GAAG,CAAC,CAAC;IACnB,iBAAiB,GAAG,KAAK,CAAC;IAC1B,MAAM,GAAqB,IAAI,CAAC;IAChC,UAAU,GAAkB,IAAI,CAAC;IACjC,SAAS,GAAkB,IAAI,CAAC;IAChC,iBAAiB,GAAG,KAAK,CAAC;IAC1B,eAAe,GAAyC,IAAI,CAAC;IAC7D,uBAAuB,GAAyC,IAAI,CAAC;IACrE,OAAO,GAAuE,IAAI,CAAC;IAEnF,YAAY,GAAoD,IAAI,CAAC;IACrE,cAAc,GAAsD,IAAI,CAAC;IACzE,aAAa,GAAwB,IAAI,CAAC;IAC1C,iBAAiB,GAAwB,IAAI,CAAC;IAE9C;QACE,IAAI,YAAY,CAAC,QAAQ;YAAE,OAAO,YAAY,CAAC,QAAQ,CAAC;QACxD,YAAY,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAE9B,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QAE/C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,oFAAoF;gBACpF,uFAAuF;gBACvF,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QAED,IAAI,CAAC,iBAAiB,GAAG,UAAU,KAAK,MAAM,IAAI,UAAU,KAAK,IAAI,CAAC;QAEtE,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QACtC,CAAC;QAED,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;YAAE,OAAO;QAE5C,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QAEtC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,oFAAoF;gBACpF,uGAAuG,CACxG,CAAC;QACJ,CAAC;QAED,MAAM,iBAAiB,CAAC,UAAU,EAAE,CAAC,mBAA4B,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;YAC7E,MAAM,YAAY,GAAG,mBAAgD,CAAC;YACtE,MAAM,QAAQ,GAAG,IAAI,YAAY,EAAE,CAAC;YAEpC,IAAI,OAAO,QAAQ,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;gBAC1C,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC;gBAC3B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC;YACjC,CAAC;QACH,CAAC,EAAE,EAAE,mBAAmB,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,IAAI,CAAC,UAAU;YAAE,GAAG,CAAC,MAAM,CAAC,2BAA2B,CAAC,CAAC;QAC7D,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;YACtD,IAAI,CAAC,OAAO,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;YAEnC,GAAG,CAAC,MAAM,CAAC,gCAAgC,OAAO,EAAE,CAAC,CAAC;YACtD,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;YACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YAErB,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7D,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAY,EAAE,MAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;YAC5F,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACtB,GAAG,CAAC,MAAM,CAAC,mCAAmC,CAAC,CAAC;gBAChD,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC,CAAC;YACzD,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;gBACrB,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;gBAC/B,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,QAAQ,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;YACvE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,CAAC,OAAwB;QAChC,IAAI,CAAC;YACH,IAAI,MAAqB,CAAC;YAE1B,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,CAAC;gBAC9C,IAAI,CAAC,GAAG;oBAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;gBACrE,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAiB,CAAC,CAAC;gBAChF,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAkB,CAAC;YAC1D,CAAC;iBAAM,CAAC;gBACN,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;gBACpE,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAa,CAAkB,CAAC;YACtD,CAAC;YAED,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;YAEjD,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;gBACvB,IAAI,UAAU,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBACzC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;gBACtD,CAAC;gBACD,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrB,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;gBACxB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;YACtB,CAAC;YAED,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;gBAC5B,IAAI,CAAC,aAAa,EAAE,CAAC;gBACrB,OAAO;YACT,CAAC;YAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;YAEvF,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,GAAG,CAAC,MAAM,CAAC,4BAA4B,OAAO,EAAE,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YAED,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAI,OAAmC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;QAC3F,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,MAAM,CAAC,6CAA6C,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAsB,EAAE,YAAY,GAAG,KAAK;QAC/C,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3B,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;YAC5D,IAAI,CAAC,GAAG;gBAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACrE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;YACnD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAED,SAAS;QACP,IAAI,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,aAAa;QACX,IAAI,IAAI,CAAC,uBAAuB;YAAE,YAAY,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAC7E,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QACpC,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QAC7C,IAAI,CAAC,eAAe,GAAG,UAAU,CAAC,GAAG,EAAE;YACrC,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,IAAI,CAAC,uBAAuB,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC7C,GAAG,CAAC,MAAM,CAAC,uDAAuD,CAAC,CAAC;gBACpE,IAAI,IAAI,CAAC,MAAM;oBAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACvC,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACxB,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACxB,CAAC;IAED,uFAAuF;IACvF,OAAO,CAAC,IAAa,EAAE,MAAe;QACpC,GAAG,CAAC,MAAM,CAAC,0CAA0C,IAAI,IAAI,SAAS,aAAa,MAAM,IAAI,MAAM,GAAG,CAAC,CAAC;QACxG,IAAI,IAAI,CAAC,eAAe;YAAE,YAAY,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,uBAAuB;YAAE,YAAY,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAC7E,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QAEpC,IAAI,CAAC,YAAY,EAAE,CAAC,IAAI,IAAI,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC;QAEhD,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC5B,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC9B,IAAI,IAAI,CAAC,eAAe;YAAE,YAAY,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC7D,IAAI,IAAI,CAAC,uBAAuB;YAAE,YAAY,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC;QAC7E,IAAI,CAAC,uBAAuB,GAAG,IAAI,CAAC;QACpC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;IACH,CAAC;IAED,iBAAiB;QACf,MAAM,EACJ,kBAAkB,GAAG,IAAI,EACzB,iBAAiB,GAAG,KAAK,GAC1B,GAAG,MAAM,CAAC,OAAO,CAAC;QAEnB,MAAM,WAAW,GAAG,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;QAChD,OAAO,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,EAAE,oBAAoB,GAAG,QAAQ,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC;QAE3D,IAAI,CAAC,cAAc,EAAE,CAAC;QAEtB,IAAI,IAAI,CAAC,cAAc,GAAG,oBAAoB,EAAE,CAAC;YAC/C,GAAG,CAAC,MAAM,CAAC,iCAAiC,CAAC,CAAC;YAC9C,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvC,IAAI,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QAClD,GAAG,CAAC,MAAM,CAAC,yBAAyB,IAAI,CAAC,cAAc,WAAW,KAAK,KAAK,CAAC,CAAC;QAE9E,MAAM,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;QAE1B,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACzB,CAAC;QAAC,MAAM,CAAC;YACP,2DAA2D;QAC7D,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,cAAc,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,iBAAiB,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAC9B,YAAY,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -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.71",
|
|
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.101",
|
|
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.28",
|
|
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
|
-
}
|