@stonyx/sockets 0.1.1-alpha.0 → 0.1.1-alpha.10
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 +4 -0
- package/config/environment.js +8 -1
- package/dist/client.d.ts +45 -0
- package/dist/client.js +176 -0
- package/dist/encryption.d.ts +4 -0
- package/dist/encryption.js +27 -0
- package/dist/handler.d.ts +5 -0
- package/{src → dist}/handler.js +1 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +18 -0
- package/dist/server.d.ts +48 -0
- package/dist/server.js +169 -0
- package/package.json +32 -11
- 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/src/client.js +0 -152
- package/src/encryption.js +0 -31
- package/src/main.js +0 -19
- package/src/server.js +0 -175
package/.claude/handlers.md
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
# Handlers
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Handlers are the primary extension point for consumers. Each handler is a class that extends `Handler` and defines a `server()` method, a `client()` method, or both. Handler files live in the handler directory (default: `./socket-handlers`).
|
|
6
|
-
|
|
7
|
-
## Base Class
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
// src/handler.js
|
|
11
|
-
export default class Handler {
|
|
12
|
-
static skipAuth = false;
|
|
13
|
-
}
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
That's it — 3 lines. The base class exists to provide the `skipAuth` default and a common prototype for `instanceof` checks.
|
|
17
|
-
|
|
18
|
-
## Defining a Handler
|
|
19
|
-
|
|
20
|
-
### Server-only handler
|
|
21
|
-
|
|
22
|
-
```javascript
|
|
23
|
-
import { Handler } from '@stonyx/sockets';
|
|
24
|
-
|
|
25
|
-
export default class ValidateGameHandler extends Handler {
|
|
26
|
-
server(data, client) {
|
|
27
|
-
// data = whatever the client sent in the 'data' field
|
|
28
|
-
// client = the WebSocket client object (with .id, .ip, .meta, .send())
|
|
29
|
-
|
|
30
|
-
// Return a value to send it back as the response
|
|
31
|
-
return { valid: true };
|
|
32
|
-
|
|
33
|
-
// Return undefined/null to send no response
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Client-only handler
|
|
39
|
-
|
|
40
|
-
```javascript
|
|
41
|
-
import { Handler } from '@stonyx/sockets';
|
|
42
|
-
|
|
43
|
-
export default class ScanGamesHandler extends Handler {
|
|
44
|
-
client(response) {
|
|
45
|
-
// response = whatever the server sent back
|
|
46
|
-
// this.client = reference to the SocketClient instance
|
|
47
|
-
|
|
48
|
-
this.client.app.scanGames(response);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Dual handler (both sides)
|
|
54
|
-
|
|
55
|
-
```javascript
|
|
56
|
-
import { Handler } from '@stonyx/sockets';
|
|
57
|
-
|
|
58
|
-
export default class EchoHandler extends Handler {
|
|
59
|
-
server(data) {
|
|
60
|
-
return data; // echo back
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
client(response) {
|
|
64
|
-
console.log('Got echo:', response);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Auth Handler (Required)
|
|
70
|
-
|
|
71
|
-
The `auth` handler is special — `SocketServer.init()` throws if it doesn't find a handler named `auth` with a `server()` method.
|
|
72
|
-
|
|
73
|
-
```javascript
|
|
74
|
-
import { Handler } from '@stonyx/sockets';
|
|
75
|
-
import config from 'stonyx/config';
|
|
76
|
-
|
|
77
|
-
export default class AuthHandler extends Handler {
|
|
78
|
-
static skipAuth = true; // MUST be true — auth runs before authentication
|
|
79
|
-
|
|
80
|
-
server(data, client) {
|
|
81
|
-
if (data.authKey !== config.sockets.authKey) return client.close();
|
|
82
|
-
|
|
83
|
-
// Register client in the server's client map
|
|
84
|
-
this._serverRef.clientMap.set(client.id, client);
|
|
85
|
-
|
|
86
|
-
// Optionally set app-level metadata
|
|
87
|
-
client.meta = { role: 'worker' };
|
|
88
|
-
|
|
89
|
-
// Returning a truthy value triggers the framework to:
|
|
90
|
-
// 1. Set client.__authenticated = true
|
|
91
|
-
// 2. Generate and send a per-session encryption key (if encryption enabled)
|
|
92
|
-
// 3. Send the response back to the client
|
|
93
|
-
return 'success';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
client(response) {
|
|
97
|
-
// this.client = the SocketClient instance
|
|
98
|
-
if (response !== 'success') this.client.promise.reject(response);
|
|
99
|
-
this.client.promise.resolve();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Auth enforcement rules
|
|
105
|
-
|
|
106
|
-
- If a message arrives from an unauthenticated client:
|
|
107
|
-
- And the handler is the `auth` handler → allowed
|
|
108
|
-
- And the handler has `static skipAuth = true` → allowed
|
|
109
|
-
- Otherwise → rejected, connection closed
|
|
110
|
-
- The framework sets `client.__authenticated = true` when the auth handler returns a truthy value
|
|
111
|
-
- If the auth handler returns `undefined`/`null`/falsy, auth fails (no response sent)
|
|
112
|
-
|
|
113
|
-
## Handler Discovery
|
|
114
|
-
|
|
115
|
-
Both `SocketServer` and `SocketClient` call `forEachFileImport` on the handler directory:
|
|
116
|
-
|
|
117
|
-
```javascript
|
|
118
|
-
await forEachFileImport(handlerDir, (HandlerClass, { name }) => {
|
|
119
|
-
const instance = new HandlerClass();
|
|
120
|
-
if (typeof instance.server === 'function') {
|
|
121
|
-
instance._serverRef = this;
|
|
122
|
-
this.handlers[name] = instance;
|
|
123
|
-
}
|
|
124
|
-
}, { ignoreAccessFailure: true });
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
- **Filename → handler name:** kebab-case to camelCase (`validate-game.js` → `validateGame`)
|
|
128
|
-
- **Exception:** `auth.js` stays as `auth`
|
|
129
|
-
- **ignoreAccessFailure:** If the handler directory doesn't exist, no error is thrown
|
|
130
|
-
|
|
131
|
-
## Handler Context
|
|
132
|
-
|
|
133
|
-
### Inside `server()` hooks
|
|
134
|
-
|
|
135
|
-
- `this._serverRef` — the `SocketServer` instance
|
|
136
|
-
- First argument `data` — the parsed `data` field from the message
|
|
137
|
-
- Second argument `client` — the WebSocket client object
|
|
138
|
-
|
|
139
|
-
### Inside `client()` hooks
|
|
140
|
-
|
|
141
|
-
- `this.client` — the `SocketClient` instance (set via `.call()` binding)
|
|
142
|
-
- First argument `response` — the parsed `response` field from the message
|
|
143
|
-
|
|
144
|
-
## Wire Protocol
|
|
145
|
-
|
|
146
|
-
All messages are JSON objects with a `request` field:
|
|
147
|
-
|
|
148
|
-
```javascript
|
|
149
|
-
// Client → Server (outgoing request)
|
|
150
|
-
{ request: 'handlerName', data: { ... } }
|
|
151
|
-
|
|
152
|
-
// Server → Client (response from handler return value)
|
|
153
|
-
{ request: 'handlerName', response: { ... } }
|
|
154
|
-
|
|
155
|
-
// Server → Client (explicit send via sendTo/broadcast)
|
|
156
|
-
{ request: 'handlerName', data: { ... } }
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Built-in Handlers
|
|
160
|
-
|
|
161
|
-
These are handled by the framework — consumers do NOT define handlers for them:
|
|
162
|
-
|
|
163
|
-
### heartBeat
|
|
164
|
-
|
|
165
|
-
- **Server:** Receives `heartBeat` request → responds with `{ request: 'heartBeat', response: true }`
|
|
166
|
-
- **Client:** Receives `heartBeat` response → schedules next heartbeat via `setTimeout`
|
|
167
|
-
- **Lifecycle:** Automatically started after successful auth; interval configured via `heartBeatInterval`
|
|
168
|
-
|
|
169
|
-
### auth (framework-level handling)
|
|
170
|
-
|
|
171
|
-
While consumers define the auth handler logic, the framework wraps it with:
|
|
172
|
-
- Auto-setting `client.__authenticated = true` on truthy return
|
|
173
|
-
- Auto-generating and transmitting per-session encryption keys
|
|
174
|
-
- Auto-starting the heartbeat cycle on the client side
|
package/.claude/index.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# @stonyx/sockets — Agent Documentation Index
|
|
2
|
-
|
|
3
|
-
Comprehensive reference for AI agents working on the `@stonyx/sockets` package. Start here, then drill into specific docs as needed.
|
|
4
|
-
|
|
5
|
-
## Quick Orientation
|
|
6
|
-
|
|
7
|
-
`@stonyx/sockets` is a Stonyx framework module providing WebSocket server/client with handler auto-discovery, auth enforcement, AES-256-GCM encryption, and built-in heartbeat. It follows the same conventions as `@stonyx/rest-server` and `stonyx-orm`.
|
|
8
|
-
|
|
9
|
-
## Documentation
|
|
10
|
-
|
|
11
|
-
- [architecture.md](./architecture.md) — Module structure, singleton pattern, Stonyx integration, handler discovery lifecycle
|
|
12
|
-
- [handlers.md](./handlers.md) — Handler class API, server/client hooks, auth flow, skipAuth, wire protocol
|
|
13
|
-
- [encryption.md](./encryption.md) — AES-256-GCM encryption, key derivation, handshake flow, session keys
|
|
14
|
-
- [configuration.md](./configuration.md) — All config options, env vars, defaults, how config loads via Stonyx
|
|
15
|
-
- [testing.md](./testing.md) — Test structure, running tests, sample handlers, writing new tests
|
|
16
|
-
- [api-reference.md](./api-reference.md) — Complete method/property reference for SocketServer, SocketClient, Handler
|
|
17
|
-
|
|
18
|
-
## Key Files
|
|
19
|
-
|
|
20
|
-
| File | Purpose |
|
|
21
|
-
|------|---------|
|
|
22
|
-
| `src/main.js` | Entry point — `Sockets` default class (Stonyx auto-init) + barrel exports |
|
|
23
|
-
| `src/server.js` | `SocketServer` — singleton, handler discovery, auth gate, message dispatch |
|
|
24
|
-
| `src/client.js` | `SocketClient` — singleton, handler discovery, connect/auth/heartbeat |
|
|
25
|
-
| `src/handler.js` | `Handler` base class (3 lines — just `skipAuth` flag) |
|
|
26
|
-
| `src/encryption.js` | AES-256-GCM encrypt/decrypt, key derivation, session key generation |
|
|
27
|
-
| `config/environment.js` | Default config with env var overrides |
|
|
28
|
-
|
|
29
|
-
## Conventions
|
|
30
|
-
|
|
31
|
-
- **Singleton pattern:** `if (Class.instance) return Class.instance;` in constructor
|
|
32
|
-
- **Stonyx module keywords:** `stonyx-async` + `stonyx-module` in package.json
|
|
33
|
-
- **Config namespace:** `config.sockets` (camelCase of package name minus `@stonyx/`)
|
|
34
|
-
- **Logging:** `log.socket()` via `logColor: 'white'` + `logMethod: 'socket'` in config
|
|
35
|
-
- **Handler discovery:** `forEachFileImport` from `@stonyx/utils/file`, kebab-to-camelCase naming
|
|
36
|
-
- **Test runner:** `stonyx test` (not plain `qunit`) — bootstraps Stonyx before running tests
|
package/.claude/testing.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# Testing
|
|
2
|
-
|
|
3
|
-
## Running Tests
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# From the stonyx-sockets directory
|
|
7
|
-
npx stonyx test
|
|
8
|
-
|
|
9
|
-
# Or via pnpm
|
|
10
|
-
pnpm test
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
**Important:** Use `stonyx test`, not plain `qunit`. The Stonyx test runner bootstraps the framework (config, logging, module init) before running QUnit. Without it, `stonyx/config` and `log.socket()` won't be available.
|
|
14
|
-
|
|
15
|
-
## Test Structure
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
test/
|
|
19
|
-
├── config/
|
|
20
|
-
│ └── environment.js # Test-specific config overrides
|
|
21
|
-
├── sample/
|
|
22
|
-
│ └── socket-handlers/
|
|
23
|
-
│ ├── auth.js # Sample auth handler (server + client hooks)
|
|
24
|
-
│ └── echo.js # Simple echo handler (both hooks)
|
|
25
|
-
├── unit/
|
|
26
|
-
│ ├── handler-test.js # Base Handler class tests
|
|
27
|
-
│ ├── encryption-test.js # AES-256-GCM encrypt/decrypt tests
|
|
28
|
-
│ ├── server-test.js # SocketServer unit tests (no network)
|
|
29
|
-
│ └── client-test.js # SocketClient unit tests (no network)
|
|
30
|
-
└── integration/
|
|
31
|
-
└── socket-test.js # Full server+client round-trip tests
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Test Config
|
|
35
|
-
|
|
36
|
-
```javascript
|
|
37
|
-
// test/config/environment.js
|
|
38
|
-
export default {
|
|
39
|
-
sockets: {
|
|
40
|
-
handlerDir: './test/sample/socket-handlers',
|
|
41
|
-
heartBeatInterval: 60000, // Long interval so timers don't fire during tests
|
|
42
|
-
encryption: 'false', // Disabled for test simplicity
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Sample Handlers
|
|
48
|
-
|
|
49
|
-
### auth.js
|
|
50
|
-
|
|
51
|
-
Validates `authKey` against config, registers client in `clientMap`, resolves the connection promise. Has `static skipAuth = true`.
|
|
52
|
-
|
|
53
|
-
### echo.js
|
|
54
|
-
|
|
55
|
-
Server returns whatever data it receives. Client stores the response on `client._lastEchoResponse` for test assertions.
|
|
56
|
-
|
|
57
|
-
## Writing Unit Tests
|
|
58
|
-
|
|
59
|
-
Unit tests do NOT start a WebSocket server. They test class behavior directly:
|
|
60
|
-
|
|
61
|
-
```javascript
|
|
62
|
-
import QUnit from 'qunit';
|
|
63
|
-
import SocketServer from '../../src/server.js';
|
|
64
|
-
|
|
65
|
-
const { module, test } = QUnit;
|
|
66
|
-
|
|
67
|
-
module('[Unit] SocketServer', function (hooks) {
|
|
68
|
-
hooks.afterEach(function () {
|
|
69
|
-
const server = SocketServer.instance;
|
|
70
|
-
if (server) server.reset();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('Singleton pattern', function (assert) {
|
|
74
|
-
const s1 = new SocketServer();
|
|
75
|
-
const s2 = new SocketServer();
|
|
76
|
-
assert.strictEqual(s1, s2);
|
|
77
|
-
s1.reset();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Key patterns:
|
|
83
|
-
- Always call `reset()` in `afterEach` to clear the singleton
|
|
84
|
-
- Use `sinon` for stubs/spies when needed
|
|
85
|
-
- Restore sinon in `afterEach` with `sinon.restore()`
|
|
86
|
-
|
|
87
|
-
## Writing Integration Tests
|
|
88
|
-
|
|
89
|
-
Integration tests start a real server and client:
|
|
90
|
-
|
|
91
|
-
```javascript
|
|
92
|
-
import QUnit from 'qunit';
|
|
93
|
-
import SocketServer from '../../src/server.js';
|
|
94
|
-
import SocketClient from '../../src/client.js';
|
|
95
|
-
import { setupIntegrationTests } from 'stonyx/test-helpers';
|
|
96
|
-
|
|
97
|
-
const { module, test } = QUnit;
|
|
98
|
-
|
|
99
|
-
module('[Integration] Sockets', function (hooks) {
|
|
100
|
-
setupIntegrationTests(hooks); // Waits for Stonyx.ready
|
|
101
|
-
|
|
102
|
-
hooks.afterEach(function () {
|
|
103
|
-
const client = SocketClient.instance;
|
|
104
|
-
const server = SocketServer.instance;
|
|
105
|
-
if (client) client.reset();
|
|
106
|
-
if (server) server.reset();
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('Round-trip', async function (assert) {
|
|
110
|
-
const server = new SocketServer();
|
|
111
|
-
await server.init();
|
|
112
|
-
|
|
113
|
-
const client = new SocketClient();
|
|
114
|
-
await client.init();
|
|
115
|
-
|
|
116
|
-
client.send({ request: 'echo', data: { msg: 'hello' } });
|
|
117
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
|
118
|
-
|
|
119
|
-
assert.deepEqual(client._lastEchoResponse, { msg: 'hello' });
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Key patterns:
|
|
125
|
-
- `setupIntegrationTests(hooks)` — adds a `hooks.before` that `await Stonyx.ready`
|
|
126
|
-
- Always clean up in `afterEach` — `reset()` terminates connections and clears state
|
|
127
|
-
- Use `setTimeout` + `await` for async message assertions (messages are async)
|
|
128
|
-
- For multiple clients: null out `SocketClient.instance` between creations, track extras for cleanup
|
|
129
|
-
|
|
130
|
-
## Common Gotchas
|
|
131
|
-
|
|
132
|
-
- **Process hangs after tests:** Usually caused by un-cleared heartbeat timers or unclosed WebSocket servers. Ensure `reset()` is called for all instances.
|
|
133
|
-
- **`log.socket is not a function`:** Running `qunit` directly instead of `stonyx test`. The Stonyx bootstrap is required.
|
|
134
|
-
- **`moduleClass is not a constructor`:** The `src/main.js` default export must be a class (not just named exports). The `Sockets` class serves as the Stonyx auto-init entry point.
|
|
135
|
-
- **Port conflicts:** Integration tests use port 2667 by default. If tests run in parallel with other services, override `SOCKET_PORT`.
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [dev, main]
|
|
6
|
-
|
|
7
|
-
concurrency:
|
|
8
|
-
group: ci-${{ github.head_ref || github.ref }}
|
|
9
|
-
cancel-in-progress: true
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
contents: read
|
|
13
|
-
|
|
14
|
-
jobs:
|
|
15
|
-
test:
|
|
16
|
-
uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
repository_dispatch:
|
|
5
|
-
types: [cascade-publish]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
inputs:
|
|
8
|
-
version-type:
|
|
9
|
-
description: 'Version type'
|
|
10
|
-
required: true
|
|
11
|
-
type: choice
|
|
12
|
-
options:
|
|
13
|
-
- patch
|
|
14
|
-
- minor
|
|
15
|
-
- major
|
|
16
|
-
custom-version:
|
|
17
|
-
description: 'Custom version (optional, overrides version-type)'
|
|
18
|
-
required: false
|
|
19
|
-
type: string
|
|
20
|
-
pull_request:
|
|
21
|
-
types: [opened, synchronize, reopened]
|
|
22
|
-
branches: [main]
|
|
23
|
-
push:
|
|
24
|
-
branches: [main]
|
|
25
|
-
|
|
26
|
-
concurrency:
|
|
27
|
-
group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
|
|
28
|
-
cancel-in-progress: false
|
|
29
|
-
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
id-token: write
|
|
33
|
-
pull-requests: write
|
|
34
|
-
|
|
35
|
-
jobs:
|
|
36
|
-
publish:
|
|
37
|
-
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
38
|
-
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
39
|
-
with:
|
|
40
|
-
version-type: ${{ github.event.inputs.version-type }}
|
|
41
|
-
custom-version: ${{ github.event.inputs.custom-version }}
|
|
42
|
-
cascade-source: ${{ github.event.client_payload.source_package || '' }}
|
|
43
|
-
secrets: inherit
|
|
44
|
-
|
|
45
|
-
cascade:
|
|
46
|
-
needs: publish
|
|
47
|
-
uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
|
|
48
|
-
with:
|
|
49
|
-
package-name: ${{ needs.publish.outputs.package-name }}
|
|
50
|
-
published-version: ${{ needs.publish.outputs.published-version }}
|
|
51
|
-
secrets: inherit
|
package/.npmignore
DELETED