@stonyx/sockets 0.1.1-alpha.3 → 0.1.1-alpha.31

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 CHANGED
@@ -1,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-sockets/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-sockets/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/sockets.svg)](https://www.npmjs.com/package/@stonyx/sockets)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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.
@@ -201,11 +205,28 @@ Configuration is read from `stonyx/config` under `sockets`:
201
205
  | Option | Env Var | Default | Description |
202
206
  |--------|---------|---------|-------------|
203
207
  | `port` | `SOCKET_PORT` | `2667` | WebSocket server port |
204
- | `address` | `SOCKET_ADDRESS` | `ws://localhost:2667` | Client connection address |
208
+ | `address` | `SOCKET_ADDRESS` | `ws://localhost:{port}` | Client connection address |
205
209
  | `authKey` | `SOCKET_AUTH_KEY` | `'AUTH_KEY'` | Shared authentication key |
206
210
  | `heartBeatInterval` | `SOCKET_HEARTBEAT_INTERVAL` | `30000` | Heartbeat interval in ms |
207
211
  | `handlerDir` | `SOCKET_HANDLER_DIR` | `'./socket-handlers'` | Handler directory path |
212
+ | `log` | `SOCKET_LOG` | `false` | Enable verbose logging |
208
213
  | `encryption` | `SOCKET_ENCRYPTION` | `'true'` | Enable AES-256-GCM encryption |
214
+ | `reconnectBaseDelay` | `SOCKET_RECONNECT_BASE_DELAY` | `1000` | Base delay in ms for reconnect backoff |
215
+ | `reconnectMaxDelay` | `SOCKET_RECONNECT_MAX_DELAY` | `60000` | Ceiling in ms for reconnect backoff |
216
+ | `maxReconnectAttempts` | `SOCKET_MAX_RECONNECT_ATTEMPTS` | `Infinity` | Reconnect attempts before giving up |
217
+
218
+ That is every variable `config/environment.js` reads -- all ten. `authData`
219
+ (`{}`), `logColor` (`'white'`) and `logMethod` (`'socket'`) are also part of the
220
+ resolved config but are not settable from the environment. No environment value
221
+ is coerced: each resolves as the string the shell supplied.
222
+
223
+ > **If your app consumes this package and runs its own test suite:** pin all ten
224
+ > of these in your own `test/config/environment.js`, and keep `port` and
225
+ > `address` coupled -- `address` is computed from `port` at module-eval time, so
226
+ > pinning `port` alone does not move it. This package reads these variables
227
+ > unconditionally, so a `NODE_ENV=test` run in a shell that exports
228
+ > `SOCKET_ADDRESS` will dial whatever it names and send `authKey` to it. See
229
+ > [docs/configuration.md](docs/configuration.md#why-all-ten-keys-are-pinned).
209
230
 
210
231
  ## API Reference
211
232
 
@@ -228,7 +249,7 @@ Configuration is read from `stonyx/config` under `sockets`:
228
249
  | `new SocketClient()` | Singleton constructor |
229
250
  | `async init()` | Discover handlers, connect, authenticate |
230
251
  | `send(payload)` | Send a message to the server |
231
- | `close()` | Close the connection |
252
+ | `close()` | Forcefully tear down the socket (remove listeners, terminate, null) |
232
253
  | `reconnect()` | Reconnect (max 5 retries) |
233
254
  | `reset()` | Close + clear all state (for testing) |
234
255
 
@@ -13,7 +13,7 @@ const {
13
13
 
14
14
  const port = SOCKET_PORT ?? 2667;
15
15
 
16
- export default {
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;
@@ -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,6 @@
1
+ export default class Handler {
2
+ static skipAuth: boolean;
3
+ server?: (data: unknown, client: unknown) => unknown | Promise<unknown>;
4
+ client?: (response: unknown) => void;
5
+ }
6
+ //# sourceMappingURL=handler.d.ts.map
@@ -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"}
@@ -0,0 +1,4 @@
1
+ export default class Handler {
2
+ static skipAuth = false;
3
+ }
4
+ //# sourceMappingURL=handler.js.map
@@ -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
@@ -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"}
@@ -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-alpha.3",
7
+ "version": "0.1.1-alpha.31",
8
8
  "description": "WebSocket server and client module for the Stonyx framework",
9
- "main": "src/main.js",
9
+ "main": "dist/main.js",
10
+ "types": "dist/main.d.ts",
10
11
  "type": "module",
11
12
  "files": [
12
- "src",
13
+ "dist",
13
14
  "config",
14
15
  "LICENSE.md",
15
16
  "README.md"
16
17
  ],
17
18
  "exports": {
18
- ".": "./src/main.js",
19
- "./server": "./src/server.js",
20
- "./client": "./src/client.js",
21
- "./handler": "./src/handler.js"
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.12",
54
+ "stonyx": "0.2.3-beta.62",
42
55
  "ws": "^8.18.1"
43
56
  },
44
57
  "devDependencies": {
45
- "@stonyx/utils": "0.2.3-beta.7",
58
+ "@stonyx/utils": "0.2.3-beta.23",
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
- "test": "stonyx test"
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
@@ -1,3 +0,0 @@
1
- export default class Handler {
2
- static skipAuth = false;
3
- }
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
- }