@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.
@@ -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)