@stonyx/sockets 0.1.0 → 0.1.1-alpha.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/.claude/api-reference.md +247 -0
- package/.claude/architecture.md +117 -0
- package/.claude/configuration.md +92 -0
- package/.claude/encryption.md +90 -0
- package/.claude/handlers.md +174 -0
- package/.claude/index.md +36 -0
- package/.claude/testing.md +135 -0
- package/.github/workflows/ci.yml +16 -0
- package/.github/workflows/publish.yml +51 -0
- package/README.md +249 -3
- package/package.json +3 -2
- package/pnpm-lock.yaml +387 -0
- package/src/client.js +12 -2
- package/src/server.js +7 -0
|
@@ -0,0 +1,247 @@
|
|
|
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)` |
|
|
@@ -0,0 +1,117 @@
|
|
|
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 |
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
## How Config Loads
|
|
4
|
+
|
|
5
|
+
The package provides `config/environment.js` with defaults. Stonyx merges this into `config.sockets`:
|
|
6
|
+
|
|
7
|
+
1. Stonyx reads `config/environment.js` (the raw export)
|
|
8
|
+
2. Wraps it under the `sockets` key (derived from package name `@stonyx/sockets` → `sockets`)
|
|
9
|
+
3. Merges any user overrides from the consumer app's environment config
|
|
10
|
+
4. In test mode, merges `test/config/environment.js` on top
|
|
11
|
+
|
|
12
|
+
Access in code: `import config from 'stonyx/config'` → `config.sockets.port`, etc.
|
|
13
|
+
|
|
14
|
+
## Config Options
|
|
15
|
+
|
|
16
|
+
| Key | Env Var | Default | Type | Description |
|
|
17
|
+
|-----|---------|---------|------|-------------|
|
|
18
|
+
| `port` | `SOCKET_PORT` | `2667` | Number | WebSocket server listening port |
|
|
19
|
+
| `address` | `SOCKET_ADDRESS` | `ws://localhost:{port}` | String | Client connection URL |
|
|
20
|
+
| `authKey` | `SOCKET_AUTH_KEY` | `'AUTH_KEY'` | String | Shared secret for authentication |
|
|
21
|
+
| `heartBeatInterval` | `SOCKET_HEARTBEAT_INTERVAL` | `30000` | Number | Heartbeat interval in milliseconds |
|
|
22
|
+
| `handlerDir` | `SOCKET_HANDLER_DIR` | `'./socket-handlers'` | String | Path to handler files directory |
|
|
23
|
+
| `log` | `SOCKET_LOG` | `false` | Boolean | Enable verbose logging (unused currently) |
|
|
24
|
+
| `encryption` | `SOCKET_ENCRYPTION` | `'true'` | String | `'true'` or `'false'` — enables AES-256-GCM |
|
|
25
|
+
|
|
26
|
+
### Logging config (framework-internal)
|
|
27
|
+
|
|
28
|
+
| Key | Value | Purpose |
|
|
29
|
+
|-----|-------|---------|
|
|
30
|
+
| `logColor` | `'white'` | Chronicle log color for `log.socket()` |
|
|
31
|
+
| `logMethod` | `'socket'` | Creates `log.socket()` method |
|
|
32
|
+
|
|
33
|
+
## Default Config File
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
// config/environment.js
|
|
37
|
+
const {
|
|
38
|
+
SOCKET_AUTH_KEY,
|
|
39
|
+
SOCKET_PORT,
|
|
40
|
+
SOCKET_ADDRESS,
|
|
41
|
+
SOCKET_HEARTBEAT_INTERVAL,
|
|
42
|
+
SOCKET_HANDLER_DIR,
|
|
43
|
+
SOCKET_LOG,
|
|
44
|
+
SOCKET_ENCRYPTION
|
|
45
|
+
} = process.env;
|
|
46
|
+
|
|
47
|
+
const port = SOCKET_PORT ?? 2667;
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
port,
|
|
51
|
+
address: SOCKET_ADDRESS ?? `ws://localhost:${port}`,
|
|
52
|
+
authKey: SOCKET_AUTH_KEY ?? 'AUTH_KEY',
|
|
53
|
+
heartBeatInterval: SOCKET_HEARTBEAT_INTERVAL ?? 30000,
|
|
54
|
+
handlerDir: SOCKET_HANDLER_DIR ?? './socket-handlers',
|
|
55
|
+
log: SOCKET_LOG ?? false,
|
|
56
|
+
logColor: 'white',
|
|
57
|
+
logMethod: 'socket',
|
|
58
|
+
encryption: SOCKET_ENCRYPTION ?? 'true',
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Consumer Override Example
|
|
63
|
+
|
|
64
|
+
In a consumer app's `config/environment.js`:
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
export default {
|
|
68
|
+
sockets: {
|
|
69
|
+
port: 3000,
|
|
70
|
+
authKey: process.env.MY_SECRET_KEY,
|
|
71
|
+
handlerDir: './my-handlers',
|
|
72
|
+
encryption: 'false',
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Only the keys you specify are overridden — the rest keep their defaults via Stonyx's `mergeObject`.
|
|
78
|
+
|
|
79
|
+
## Test Config Override
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
// test/config/environment.js
|
|
83
|
+
export default {
|
|
84
|
+
sockets: {
|
|
85
|
+
handlerDir: './test/sample/socket-handlers',
|
|
86
|
+
heartBeatInterval: 60000,
|
|
87
|
+
encryption: 'false',
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Note: Test overrides use the namespaced key (`sockets: { ... }`) because they're merged after Stonyx has already namespaced the config.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Encryption
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Encryption is enabled by default (`SOCKET_ENCRYPTION=true`). All message encryption uses Node.js native `crypto` with zero external dependencies.
|
|
6
|
+
|
|
7
|
+
- **Algorithm:** AES-256-GCM (authenticated encryption)
|
|
8
|
+
- **IV:** 12 bytes (GCM recommended)
|
|
9
|
+
- **Auth tag:** 16 bytes
|
|
10
|
+
- **Key size:** 256-bit (32 bytes)
|
|
11
|
+
|
|
12
|
+
## Key Derivation
|
|
13
|
+
|
|
14
|
+
The user-provided `authKey` string (from config) is run through `crypto.scryptSync` to derive a proper 32-byte AES key:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
export function deriveKey(authKey) {
|
|
18
|
+
return crypto.scryptSync(authKey, 'stonyx-sockets', 32);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This ensures even short/weak auth key strings produce valid 256-bit encryption keys. The salt `'stonyx-sockets'` is fixed (deterministic derivation).
|
|
23
|
+
|
|
24
|
+
## Wire Format
|
|
25
|
+
|
|
26
|
+
Encrypted messages are sent as binary buffers:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
[ IV (12 bytes) ][ Auth Tag (16 bytes) ][ Ciphertext (variable) ]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Handshake Flow
|
|
33
|
+
|
|
34
|
+
1. Client connects and sends the `auth` request encrypted with the **global key** (derived from `authKey`)
|
|
35
|
+
2. Server decrypts using the global key, validates credentials via the auth handler
|
|
36
|
+
3. Server generates a **per-session key** (`crypto.randomBytes(32)`) for this client
|
|
37
|
+
4. Server responds with auth success + the session key (base64-encoded), encrypted with the global key
|
|
38
|
+
5. Client stores the session key
|
|
39
|
+
6. **All subsequent messages** (both directions) use the per-session key
|
|
40
|
+
|
|
41
|
+
### Key usage by message type
|
|
42
|
+
|
|
43
|
+
| Message | Encrypt with | Decrypt with |
|
|
44
|
+
|---------|-------------|-------------|
|
|
45
|
+
| Auth request (client → server) | Global key | Global key |
|
|
46
|
+
| Auth response (server → client) | Global key | Global key |
|
|
47
|
+
| All other messages | Session key | Session key |
|
|
48
|
+
|
|
49
|
+
## Functions (src/encryption.js)
|
|
50
|
+
|
|
51
|
+
### `encrypt(data, key) → Buffer`
|
|
52
|
+
|
|
53
|
+
Encrypts a UTF-8 string using AES-256-GCM. Returns `Buffer` of `iv + tag + ciphertext`.
|
|
54
|
+
|
|
55
|
+
### `decrypt(buffer, key) → string`
|
|
56
|
+
|
|
57
|
+
Decrypts a buffer (or base64 string) back to UTF-8. Throws on tampered data or wrong key.
|
|
58
|
+
|
|
59
|
+
### `generateSessionKey() → Buffer`
|
|
60
|
+
|
|
61
|
+
Returns `crypto.randomBytes(32)` — a random 256-bit key for per-session encryption.
|
|
62
|
+
|
|
63
|
+
### `deriveKey(authKey) → Buffer`
|
|
64
|
+
|
|
65
|
+
Derives a 32-byte key from a string using `scryptSync`.
|
|
66
|
+
|
|
67
|
+
## Integration Points
|
|
68
|
+
|
|
69
|
+
### Server (src/server.js)
|
|
70
|
+
|
|
71
|
+
- `init()`: If encryption enabled, derives `this.globalKey` from `config.sockets.authKey`
|
|
72
|
+
- `prepareSend(client)`: Wraps `client.send()` to encrypt with the client's session key (or a key override for auth)
|
|
73
|
+
- `onMessage()`: Decrypts with session key (or global key for unauthenticated clients)
|
|
74
|
+
- Auth response: Generates `client.__sessionKey`, includes base64-encoded key in the response, encrypts with global key
|
|
75
|
+
|
|
76
|
+
### Client (src/client.js)
|
|
77
|
+
|
|
78
|
+
- `init()`: If encryption enabled, derives `this.globalKey` from `config.sockets.authKey`
|
|
79
|
+
- `send()`: Encrypts with session key (or global key if `useGlobalKey=true` for auth)
|
|
80
|
+
- `onMessage()`: Decrypts with session key (or global key if no session key yet)
|
|
81
|
+
- Auth response: Stores `this.sessionKey` from the base64-encoded key in the response
|
|
82
|
+
|
|
83
|
+
## Disabling Encryption
|
|
84
|
+
|
|
85
|
+
Set `SOCKET_ENCRYPTION=false` in the environment. When disabled:
|
|
86
|
+
|
|
87
|
+
- Messages are sent as plain JSON strings
|
|
88
|
+
- No key derivation occurs
|
|
89
|
+
- No session keys are generated
|
|
90
|
+
- The `authKey` is still sent in the auth request (as plaintext JSON)
|