@stonyx/sockets 0.1.1-alpha.1 → 0.1.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/environment.js +8 -1
- package/package.json +5 -2
- package/src/client.js +57 -12
- package/src/server.js +17 -6
- package/.claude/api-reference.md +0 -247
- package/.claude/architecture.md +0 -117
- package/.claude/configuration.md +0 -92
- package/.claude/encryption.md +0 -90
- package/.claude/handlers.md +0 -174
- package/.claude/index.md +0 -36
- package/.claude/testing.md +0 -135
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/.npmignore +0 -4
- package/pnpm-lock.yaml +0 -387
package/config/environment.js
CHANGED
|
@@ -5,7 +5,10 @@ const {
|
|
|
5
5
|
SOCKET_HEARTBEAT_INTERVAL,
|
|
6
6
|
SOCKET_HANDLER_DIR,
|
|
7
7
|
SOCKET_LOG,
|
|
8
|
-
SOCKET_ENCRYPTION
|
|
8
|
+
SOCKET_ENCRYPTION,
|
|
9
|
+
SOCKET_RECONNECT_BASE_DELAY,
|
|
10
|
+
SOCKET_RECONNECT_MAX_DELAY,
|
|
11
|
+
SOCKET_MAX_RECONNECT_ATTEMPTS,
|
|
9
12
|
} = process.env;
|
|
10
13
|
|
|
11
14
|
const port = SOCKET_PORT ?? 2667;
|
|
@@ -14,10 +17,14 @@ export default {
|
|
|
14
17
|
port,
|
|
15
18
|
address: SOCKET_ADDRESS ?? `ws://localhost:${port}`,
|
|
16
19
|
authKey: SOCKET_AUTH_KEY ?? 'AUTH_KEY',
|
|
20
|
+
authData: {},
|
|
17
21
|
heartBeatInterval: SOCKET_HEARTBEAT_INTERVAL ?? 30000,
|
|
18
22
|
handlerDir: SOCKET_HANDLER_DIR ?? './socket-handlers',
|
|
19
23
|
log: SOCKET_LOG ?? false,
|
|
20
24
|
logColor: 'white',
|
|
21
25
|
logMethod: 'socket',
|
|
22
26
|
encryption: SOCKET_ENCRYPTION ?? 'true',
|
|
27
|
+
reconnectBaseDelay: SOCKET_RECONNECT_BASE_DELAY ?? 1000,
|
|
28
|
+
reconnectMaxDelay: SOCKET_RECONNECT_MAX_DELAY ?? 60000,
|
|
29
|
+
maxReconnectAttempts: SOCKET_MAX_RECONNECT_ATTEMPTS ?? Infinity,
|
|
23
30
|
};
|
package/package.json
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.1-
|
|
7
|
+
"version": "0.1.1-beta.0",
|
|
8
8
|
"description": "WebSocket server and client module for the Stonyx framework",
|
|
9
9
|
"main": "src/main.js",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
12
|
+
"src",
|
|
13
|
+
"config",
|
|
14
|
+
"LICENSE.md",
|
|
15
|
+
"README.md"
|
|
13
16
|
],
|
|
14
17
|
"exports": {
|
|
15
18
|
".": "./src/main.js",
|
package/src/client.js
CHANGED
|
@@ -2,11 +2,18 @@ import { WebSocket } from 'ws';
|
|
|
2
2
|
import config from 'stonyx/config';
|
|
3
3
|
import log from 'stonyx/log';
|
|
4
4
|
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
+
import { sleep } from '@stonyx/utils/promise';
|
|
5
6
|
import { encrypt, decrypt, deriveKey } from './encryption.js';
|
|
6
7
|
|
|
7
8
|
export default class SocketClient {
|
|
8
9
|
handlers = {};
|
|
9
10
|
reconnectCount = 0;
|
|
11
|
+
_intentionalClose = false;
|
|
12
|
+
|
|
13
|
+
onDisconnect = null;
|
|
14
|
+
onReconnecting = null;
|
|
15
|
+
onReconnected = null;
|
|
16
|
+
onReconnectFailed = null;
|
|
10
17
|
|
|
11
18
|
constructor() {
|
|
12
19
|
if (SocketClient.instance) return SocketClient.instance;
|
|
@@ -41,7 +48,7 @@ export default class SocketClient {
|
|
|
41
48
|
|
|
42
49
|
async connect() {
|
|
43
50
|
return new Promise((resolve, reject) => {
|
|
44
|
-
const { address, authKey } = config.sockets;
|
|
51
|
+
const { address, authKey, authData } = config.sockets;
|
|
45
52
|
this.promise = { resolve, reject };
|
|
46
53
|
|
|
47
54
|
log.socket(`Connecting to remote server: ${address}`);
|
|
@@ -51,13 +58,14 @@ export default class SocketClient {
|
|
|
51
58
|
socket.onmessage = this.onMessage.bind(this);
|
|
52
59
|
socket.onclose = this.onClose.bind(this);
|
|
53
60
|
socket.onerror = event => {
|
|
54
|
-
|
|
61
|
+
log.socket(`Error connecting to socket server`);
|
|
55
62
|
reject('Error connecting to socket server');
|
|
56
63
|
};
|
|
57
64
|
|
|
58
65
|
socket.onopen = () => {
|
|
66
|
+
this._intentionalClose = false;
|
|
59
67
|
this.reconnectCount = 0;
|
|
60
|
-
this.send({ request: 'auth', data: { authKey } }, true);
|
|
68
|
+
this.send({ request: 'auth', data: { authKey, ...authData } }, true);
|
|
61
69
|
};
|
|
62
70
|
});
|
|
63
71
|
}
|
|
@@ -71,12 +79,12 @@ export default class SocketClient {
|
|
|
71
79
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
72
80
|
parsed = JSON.parse(decrypt(raw, key));
|
|
73
81
|
} else {
|
|
74
|
-
|
|
82
|
+
const raw = Buffer.isBuffer(payload) ? payload.toString() : payload;
|
|
83
|
+
parsed = JSON.parse(raw);
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
const { request, response, sessionKey } = parsed;
|
|
78
87
|
|
|
79
|
-
// Built-in auth session key handling + heartbeat kickoff
|
|
80
88
|
if (request === 'auth') {
|
|
81
89
|
if (sessionKey && this.encryptionEnabled) {
|
|
82
90
|
this.sessionKey = Buffer.from(sessionKey, 'base64');
|
|
@@ -84,7 +92,6 @@ export default class SocketClient {
|
|
|
84
92
|
this.nextHeartBeat();
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
// Built-in heartbeat — schedule next beat on response
|
|
88
95
|
if (request === 'heartBeat') {
|
|
89
96
|
this.nextHeartBeat();
|
|
90
97
|
return;
|
|
@@ -93,13 +100,12 @@ export default class SocketClient {
|
|
|
93
100
|
const handler = this.handlers[request];
|
|
94
101
|
|
|
95
102
|
if (!handler) {
|
|
96
|
-
|
|
103
|
+
log.socket(`Call to invalid handler: ${request}`);
|
|
97
104
|
return;
|
|
98
105
|
}
|
|
99
106
|
|
|
100
107
|
handler.client.call({ client: this, ...handler }, response);
|
|
101
108
|
} catch (error) {
|
|
102
|
-
console.error(error);
|
|
103
109
|
log.socket(`Invalid payload received from remote server`);
|
|
104
110
|
}
|
|
105
111
|
}
|
|
@@ -125,21 +131,55 @@ export default class SocketClient {
|
|
|
125
131
|
onClose() {
|
|
126
132
|
log.socket('Disconnected from remote server');
|
|
127
133
|
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
134
|
+
|
|
135
|
+
this.onDisconnect?.();
|
|
136
|
+
|
|
137
|
+
if (!this._intentionalClose) {
|
|
138
|
+
this.reconnect();
|
|
139
|
+
}
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
close() {
|
|
143
|
+
this._intentionalClose = true;
|
|
131
144
|
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
132
145
|
if (this.socket) this.socket.close();
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
|
|
136
|
-
|
|
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) {
|
|
137
166
|
log.socket('Max reconnect attempts exceeded');
|
|
167
|
+
this.onReconnectFailed?.();
|
|
138
168
|
return;
|
|
139
169
|
}
|
|
140
170
|
|
|
141
|
-
this.
|
|
142
|
-
|
|
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
|
+
}
|
|
143
183
|
}
|
|
144
184
|
|
|
145
185
|
reset() {
|
|
@@ -147,6 +187,11 @@ export default class SocketClient {
|
|
|
147
187
|
this.handlers = {};
|
|
148
188
|
this.sessionKey = null;
|
|
149
189
|
this.reconnectCount = 0;
|
|
190
|
+
this._intentionalClose = false;
|
|
191
|
+
this.onDisconnect = null;
|
|
192
|
+
this.onReconnecting = null;
|
|
193
|
+
this.onReconnected = null;
|
|
194
|
+
this.onReconnectFailed = null;
|
|
150
195
|
SocketClient.instance = null;
|
|
151
196
|
}
|
|
152
197
|
}
|
package/src/server.js
CHANGED
|
@@ -63,7 +63,7 @@ export default class SocketServer {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
onMessage(payload, client) {
|
|
66
|
+
async onMessage(payload, client) {
|
|
67
67
|
try {
|
|
68
68
|
let parsed;
|
|
69
69
|
|
|
@@ -96,7 +96,7 @@ export default class SocketServer {
|
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
const response = handler.server(data, client);
|
|
99
|
+
const response = await handler.server(data, client);
|
|
100
100
|
if (response === undefined || response === null) return;
|
|
101
101
|
|
|
102
102
|
if (request === 'auth' && response) {
|
|
@@ -139,18 +139,29 @@ export default class SocketServer {
|
|
|
139
139
|
const { ip } = client;
|
|
140
140
|
log.socket(`[${ip}] Client disconnected`);
|
|
141
141
|
this.clientMap.delete(client.id);
|
|
142
|
+
this.onClientDisconnect?.(client);
|
|
142
143
|
}
|
|
143
144
|
|
|
144
|
-
sendTo(clientId, request,
|
|
145
|
+
sendTo(clientId, request, response) {
|
|
145
146
|
const client = this.clientMap.get(clientId);
|
|
146
147
|
if (!client) return;
|
|
147
|
-
client.send({ request,
|
|
148
|
+
client.send({ request, response });
|
|
148
149
|
}
|
|
149
150
|
|
|
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) {
|
|
151
162
|
for (const [, client] of this.clientMap) {
|
|
152
163
|
if (client.__authenticated) {
|
|
153
|
-
client.send({ request,
|
|
164
|
+
client.send({ request, response });
|
|
154
165
|
}
|
|
155
166
|
}
|
|
156
167
|
}
|
package/.claude/api-reference.md
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
# API Reference
|
|
2
|
-
|
|
3
|
-
## Exports
|
|
4
|
-
|
|
5
|
-
```javascript
|
|
6
|
-
// Main entry (src/main.js)
|
|
7
|
-
import { SocketServer, SocketClient, Handler } from '@stonyx/sockets';
|
|
8
|
-
|
|
9
|
-
// Sub-path exports
|
|
10
|
-
import SocketServer from '@stonyx/sockets/server';
|
|
11
|
-
import SocketClient from '@stonyx/sockets/client';
|
|
12
|
-
import Handler from '@stonyx/sockets/handler';
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
The default export from `@stonyx/sockets` is the `Sockets` class (used by Stonyx for auto-init). Consumer code should use the named exports.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## SocketServer
|
|
20
|
-
|
|
21
|
-
**File:** `src/server.js`
|
|
22
|
-
|
|
23
|
-
### Properties
|
|
24
|
-
|
|
25
|
-
| Property | Type | Description |
|
|
26
|
-
|----------|------|-------------|
|
|
27
|
-
| `clientMap` | `Map<number, client>` | Connected clients keyed by auto-assigned ID |
|
|
28
|
-
| `handlers` | `Object<string, Handler>` | Discovered server handlers keyed by name |
|
|
29
|
-
| `wss` | `WebSocketServer \| null` | The underlying `ws` WebSocketServer instance |
|
|
30
|
-
| `encryptionEnabled` | `boolean` | Whether AES-256-GCM encryption is active |
|
|
31
|
-
| `globalKey` | `Buffer` | Derived encryption key from authKey (if encryption enabled) |
|
|
32
|
-
|
|
33
|
-
### Static
|
|
34
|
-
|
|
35
|
-
| Property | Type | Description |
|
|
36
|
-
|----------|------|-------------|
|
|
37
|
-
| `SocketServer.instance` | `SocketServer \| null` | Singleton instance |
|
|
38
|
-
|
|
39
|
-
### Methods
|
|
40
|
-
|
|
41
|
-
#### `constructor()`
|
|
42
|
-
|
|
43
|
-
Returns the existing singleton or creates a new one. Does NOT start the server.
|
|
44
|
-
|
|
45
|
-
#### `async init()`
|
|
46
|
-
|
|
47
|
-
Full initialization sequence:
|
|
48
|
-
1. Discovers handlers from `config.sockets.handlerDir`
|
|
49
|
-
2. Validates that an `auth` handler exists
|
|
50
|
-
3. Configures encryption if enabled
|
|
51
|
-
4. Starts `WebSocketServer` on `config.sockets.port`
|
|
52
|
-
5. Wires connection/message/close events
|
|
53
|
-
|
|
54
|
-
#### `sendTo(clientId, request, data)`
|
|
55
|
-
|
|
56
|
-
Send a message to a specific client by numeric ID.
|
|
57
|
-
|
|
58
|
-
- `clientId` — number, the client's auto-assigned ID
|
|
59
|
-
- `request` — string, the handler name
|
|
60
|
-
- `data` — any serializable value
|
|
61
|
-
|
|
62
|
-
Does nothing if the client doesn't exist.
|
|
63
|
-
|
|
64
|
-
#### `broadcast(request, data)`
|
|
65
|
-
|
|
66
|
-
Send a message to all authenticated clients in `clientMap`.
|
|
67
|
-
|
|
68
|
-
- `request` — string, the handler name
|
|
69
|
-
- `data` — any serializable value
|
|
70
|
-
|
|
71
|
-
#### `close()`
|
|
72
|
-
|
|
73
|
-
Terminates all connected clients and closes the WebSocket server.
|
|
74
|
-
|
|
75
|
-
#### `reset()`
|
|
76
|
-
|
|
77
|
-
Calls `close()`, clears `clientMap`, clears `handlers`, resets the client ID counter, sets `SocketServer.instance = null`. Used in tests.
|
|
78
|
-
|
|
79
|
-
### Internal Methods
|
|
80
|
-
|
|
81
|
-
#### `async discoverHandlers()`
|
|
82
|
-
|
|
83
|
-
Scans handler directory. For each file: instantiates the class, checks for `server()` method, registers it in `this.handlers`.
|
|
84
|
-
|
|
85
|
-
#### `validateAuthHandler()`
|
|
86
|
-
|
|
87
|
-
Throws `Error` if `this.handlers.auth` doesn't exist.
|
|
88
|
-
|
|
89
|
-
#### `onMessage(payload, client)`
|
|
90
|
-
|
|
91
|
-
Main message dispatcher. Decrypts (if enabled), parses JSON, routes to handler or built-in heartbeat, enforces auth gate.
|
|
92
|
-
|
|
93
|
-
#### `prepareSend(client)`
|
|
94
|
-
|
|
95
|
-
Replaces `client.send()` with a wrapper that JSON-stringifies and encrypts (if enabled).
|
|
96
|
-
|
|
97
|
-
#### `handleDisconnect(client)`
|
|
98
|
-
|
|
99
|
-
Removes client from `clientMap`, logs disconnection.
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## SocketClient
|
|
104
|
-
|
|
105
|
-
**File:** `src/client.js`
|
|
106
|
-
|
|
107
|
-
### Properties
|
|
108
|
-
|
|
109
|
-
| Property | Type | Description |
|
|
110
|
-
|----------|------|-------------|
|
|
111
|
-
| `handlers` | `Object<string, Handler>` | Discovered client handlers keyed by name |
|
|
112
|
-
| `socket` | `WebSocket \| null` | The underlying `ws` WebSocket instance |
|
|
113
|
-
| `reconnectCount` | `number` | Current reconnection attempt count |
|
|
114
|
-
| `promise` | `{ resolve, reject }` | Connection promise callbacks |
|
|
115
|
-
| `encryptionEnabled` | `boolean` | Whether AES-256-GCM encryption is active |
|
|
116
|
-
| `globalKey` | `Buffer` | Derived encryption key from authKey (if encryption enabled) |
|
|
117
|
-
| `sessionKey` | `Buffer \| null` | Per-session key received from server after auth |
|
|
118
|
-
|
|
119
|
-
### Static
|
|
120
|
-
|
|
121
|
-
| Property | Type | Description |
|
|
122
|
-
|----------|------|-------------|
|
|
123
|
-
| `SocketClient.instance` | `SocketClient \| null` | Singleton instance |
|
|
124
|
-
|
|
125
|
-
### Methods
|
|
126
|
-
|
|
127
|
-
#### `constructor()`
|
|
128
|
-
|
|
129
|
-
Returns the existing singleton or creates a new one. Does NOT connect.
|
|
130
|
-
|
|
131
|
-
#### `async init()`
|
|
132
|
-
|
|
133
|
-
Full initialization:
|
|
134
|
-
1. Discovers handlers from `config.sockets.handlerDir`
|
|
135
|
-
2. Configures encryption if enabled
|
|
136
|
-
3. Calls `connect()`
|
|
137
|
-
|
|
138
|
-
Returns the Promise from `connect()`.
|
|
139
|
-
|
|
140
|
-
#### `send(payload, useGlobalKey = false)`
|
|
141
|
-
|
|
142
|
-
Send a message to the server.
|
|
143
|
-
|
|
144
|
-
- `payload` — object with `request` and optionally `data` fields
|
|
145
|
-
- `useGlobalKey` — boolean, use the global key instead of session key (internal, for auth)
|
|
146
|
-
|
|
147
|
-
#### `close()`
|
|
148
|
-
|
|
149
|
-
Clears heartbeat timer and closes the WebSocket connection.
|
|
150
|
-
|
|
151
|
-
#### `reconnect()`
|
|
152
|
-
|
|
153
|
-
Attempts to reconnect. Returns the `connect()` Promise. Fails after 5 consecutive attempts.
|
|
154
|
-
|
|
155
|
-
#### `reset()`
|
|
156
|
-
|
|
157
|
-
Calls `close()`, clears handlers, clears session key, resets reconnect counter, sets `SocketClient.instance = null`. Used in tests.
|
|
158
|
-
|
|
159
|
-
### Internal Methods
|
|
160
|
-
|
|
161
|
-
#### `async discoverHandlers()`
|
|
162
|
-
|
|
163
|
-
Scans handler directory. For each file: instantiates the class, checks for `client()` method, registers it.
|
|
164
|
-
|
|
165
|
-
#### `async connect()`
|
|
166
|
-
|
|
167
|
-
Creates WebSocket, wires events, sends auth on open. Returns Promise that resolves when auth handler resolves it.
|
|
168
|
-
|
|
169
|
-
#### `onMessage({ data: payload })`
|
|
170
|
-
|
|
171
|
-
Decrypts, parses, handles built-in auth/heartbeat, routes to handler.
|
|
172
|
-
|
|
173
|
-
#### `heartBeat()`
|
|
174
|
-
|
|
175
|
-
Sends `{ request: 'heartBeat' }` to the server.
|
|
176
|
-
|
|
177
|
-
#### `nextHeartBeat()`
|
|
178
|
-
|
|
179
|
-
Schedules `heartBeat()` via `setTimeout` at `config.sockets.heartBeatInterval`.
|
|
180
|
-
|
|
181
|
-
#### `onClose()`
|
|
182
|
-
|
|
183
|
-
Logs disconnection, clears heartbeat timer.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## Handler
|
|
188
|
-
|
|
189
|
-
**File:** `src/handler.js`
|
|
190
|
-
|
|
191
|
-
### Static Properties
|
|
192
|
-
|
|
193
|
-
| Property | Type | Default | Description |
|
|
194
|
-
|----------|------|---------|-------------|
|
|
195
|
-
| `skipAuth` | `boolean` | `false` | Set `true` to allow pre-auth access (server side only) |
|
|
196
|
-
|
|
197
|
-
### Instance Methods (defined by subclasses)
|
|
198
|
-
|
|
199
|
-
#### `server(data, client)`
|
|
200
|
-
|
|
201
|
-
Server-side hook. Called when a message with the matching handler name arrives.
|
|
202
|
-
|
|
203
|
-
- `data` — the parsed `data` field from the incoming message
|
|
204
|
-
- `client` — the WebSocket client object with `.id`, `.ip`, `.meta`, `.send()`, `.__authenticated`
|
|
205
|
-
- **Return value:** sent back as the `response` field. Return `undefined`/`null` to send nothing.
|
|
206
|
-
|
|
207
|
-
#### `client(response)`
|
|
208
|
-
|
|
209
|
-
Client-side hook. Called when a response with the matching handler name arrives from the server.
|
|
210
|
-
|
|
211
|
-
- `response` — the parsed `response` field from the message
|
|
212
|
-
- **Context:** `this.client` references the `SocketClient` instance
|
|
213
|
-
|
|
214
|
-
### Instance Properties (set by framework)
|
|
215
|
-
|
|
216
|
-
| Property | Set by | Description |
|
|
217
|
-
|----------|--------|-------------|
|
|
218
|
-
| `_serverRef` | `SocketServer.discoverHandlers()` | Reference to the SocketServer instance |
|
|
219
|
-
| `_clientRef` | `SocketClient.discoverHandlers()` | Reference to the SocketClient instance |
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## Client Object (Server-Side)
|
|
224
|
-
|
|
225
|
-
The raw WebSocket client is augmented by the framework:
|
|
226
|
-
|
|
227
|
-
| Property | Type | Set by | Description |
|
|
228
|
-
|----------|------|--------|-------------|
|
|
229
|
-
| `id` | `number` | Framework (on connect) | Auto-incrementing numeric ID |
|
|
230
|
-
| `ip` | `string` | Framework (on connect) | Remote IP address |
|
|
231
|
-
| `__authenticated` | `boolean` | Framework (on auth) | Auth state |
|
|
232
|
-
| `__sessionKey` | `Buffer` | Framework (on auth, if encryption) | Per-session encryption key |
|
|
233
|
-
| `meta` | `any` | Consumer (in auth handler) | App-defined metadata |
|
|
234
|
-
| `send(payload)` | `function` | Framework (`prepareSend`) | Sends JSON, auto-encrypts if enabled |
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## Encryption Functions
|
|
239
|
-
|
|
240
|
-
**File:** `src/encryption.js`
|
|
241
|
-
|
|
242
|
-
| Function | Signature | Description |
|
|
243
|
-
|----------|-----------|-------------|
|
|
244
|
-
| `encrypt` | `(data: string, key: Buffer) → Buffer` | AES-256-GCM encrypt, returns `iv + tag + ciphertext` |
|
|
245
|
-
| `decrypt` | `(buffer: Buffer, key: Buffer) → string` | AES-256-GCM decrypt, returns UTF-8 string |
|
|
246
|
-
| `generateSessionKey` | `() → Buffer` | `crypto.randomBytes(32)` |
|
|
247
|
-
| `deriveKey` | `(authKey: string) → Buffer` | `crypto.scryptSync(authKey, 'stonyx-sockets', 32)` |
|
package/.claude/architecture.md
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# Architecture
|
|
2
|
-
|
|
3
|
-
## Module Structure
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
stonyx-sockets/
|
|
7
|
-
├── config/environment.js # Default config, env var overrides
|
|
8
|
-
├── src/
|
|
9
|
-
│ ├── main.js # Sockets class (Stonyx entry) + barrel exports
|
|
10
|
-
│ ├── server.js # SocketServer singleton
|
|
11
|
-
│ ├── client.js # SocketClient singleton
|
|
12
|
-
│ ├── handler.js # Base Handler class
|
|
13
|
-
│ └── encryption.js # AES-256-GCM utilities
|
|
14
|
-
└── test/
|
|
15
|
-
├── config/environment.js # Test config overrides
|
|
16
|
-
├── sample/socket-handlers # Auth + echo sample handlers
|
|
17
|
-
├── unit/ # Handler, encryption, server, client unit tests
|
|
18
|
-
└── integration/ # Full server+client round-trip tests
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Stonyx Integration
|
|
22
|
-
|
|
23
|
-
### Auto-Initialization
|
|
24
|
-
|
|
25
|
-
The package declares `stonyx-async` + `stonyx-module` keywords. When Stonyx starts:
|
|
26
|
-
|
|
27
|
-
1. Reads `config/environment.js` and merges into `config.sockets`
|
|
28
|
-
2. Registers `log.socket()` via `logColor: 'white'` + `logMethod: 'socket'`
|
|
29
|
-
3. Imports `src/main.js`, instantiates the default `Sockets` class, calls `.init()`
|
|
30
|
-
|
|
31
|
-
The `Sockets` default export is a thin entry point — it does NOT auto-start the WebSocket server. The actual server/client initialization is deferred to when the consumer explicitly creates `new SocketServer()` or `new SocketClient()` and calls `.init()`.
|
|
32
|
-
|
|
33
|
-
### Standalone Mode (Testing)
|
|
34
|
-
|
|
35
|
-
When running `stonyx test` from within the package directory, Stonyx detects the `stonyx-` prefix in the path and transforms the config:
|
|
36
|
-
|
|
37
|
-
```javascript
|
|
38
|
-
// config/environment.js exports { port, address, authKey, ... }
|
|
39
|
-
// Stonyx wraps it as: { sockets: { port, address, authKey, ... } }
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Test overrides from `test/config/environment.js` are merged on top.
|
|
43
|
-
|
|
44
|
-
## Singleton Pattern
|
|
45
|
-
|
|
46
|
-
Both `SocketServer` and `SocketClient` use the Stonyx singleton convention:
|
|
47
|
-
|
|
48
|
-
```javascript
|
|
49
|
-
constructor() {
|
|
50
|
-
if (SocketServer.instance) return SocketServer.instance;
|
|
51
|
-
SocketServer.instance = this;
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`reset()` sets the static instance back to `null` (used in tests).
|
|
56
|
-
|
|
57
|
-
## Initialization Lifecycle
|
|
58
|
-
|
|
59
|
-
### SocketServer.init()
|
|
60
|
-
|
|
61
|
-
1. **discoverHandlers()** — scans `config.sockets.handlerDir` via `forEachFileImport`
|
|
62
|
-
- For each file: instantiates the class, checks for `server()` method
|
|
63
|
-
- Stores instance in `this.handlers[name]` (kebab-to-camelCase)
|
|
64
|
-
- Sets `instance._serverRef = this` for access to the server within handlers
|
|
65
|
-
2. **validateAuthHandler()** — throws if `this.handlers.auth` doesn't exist
|
|
66
|
-
3. **Configure encryption** — if enabled, derives global key from `authKey`
|
|
67
|
-
4. **Start WebSocketServer** on configured port
|
|
68
|
-
5. **Wire connection events** — on each connection: assign ID, set IP, wrap send, bind message/close listeners
|
|
69
|
-
|
|
70
|
-
### SocketClient.init()
|
|
71
|
-
|
|
72
|
-
1. **discoverHandlers()** — same as server, but checks for `client()` method
|
|
73
|
-
- Sets `instance._clientRef = this` for access to the client within handlers
|
|
74
|
-
2. **Configure encryption** — if enabled, derives global key from `authKey`
|
|
75
|
-
3. **connect()** — returns Promise that resolves after auth completes
|
|
76
|
-
- Creates WebSocket to `config.sockets.address`
|
|
77
|
-
- On open: sends auth request
|
|
78
|
-
- On auth response: resolves the promise, starts heartbeat
|
|
79
|
-
|
|
80
|
-
## Message Flow
|
|
81
|
-
|
|
82
|
-
### Server onMessage
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
payload received
|
|
86
|
-
→ decrypt (if encryption enabled, using session key or global key for auth)
|
|
87
|
-
→ JSON.parse
|
|
88
|
-
→ heartBeat? → respond with true, return
|
|
89
|
-
→ handler lookup by request name
|
|
90
|
-
→ auth gate: if not auth handler, not skipAuth, not authenticated → reject + close
|
|
91
|
-
→ call handler.server(data, client)
|
|
92
|
-
→ if return value is truthy:
|
|
93
|
-
→ if auth request: set __authenticated, generate session key, respond
|
|
94
|
-
→ else: send { request, response } back to client
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Client onMessage
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
payload received
|
|
101
|
-
→ decrypt (if encryption enabled, using session key or global key)
|
|
102
|
-
→ JSON.parse
|
|
103
|
-
→ auth response? → store session key, start heartbeat
|
|
104
|
-
→ heartBeat response? → schedule next heartbeat, return
|
|
105
|
-
→ handler lookup by request name
|
|
106
|
-
→ call handler.client(response) with this.client bound
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Dependencies
|
|
110
|
-
|
|
111
|
-
| Package | Purpose |
|
|
112
|
-
|---------|---------|
|
|
113
|
-
| `ws` | WebSocket server (`WebSocketServer`) and client (`WebSocket`) |
|
|
114
|
-
| `stonyx` | Framework core — config, logging, module lifecycle |
|
|
115
|
-
| `@stonyx/utils` (dev) | `forEachFileImport` for handler auto-discovery |
|
|
116
|
-
| `qunit` (dev) | Test framework |
|
|
117
|
-
| `sinon` (dev) | Stubs and spies for unit tests |
|