@aegis-fluxion/core 0.7.1 → 0.7.2
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 +98 -245
- package/dist/index.cjs +182 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +182 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# @aegis-fluxion/core
|
|
2
2
|
|
|
3
|
-
Low-level
|
|
3
|
+
Low-level encrypted WebSocket primitives for the `aegis-fluxion` ecosystem.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Version: **0.7.1**
|
|
5
|
+
Version: **0.7.2**
|
|
8
6
|
|
|
9
7
|
---
|
|
10
8
|
|
|
@@ -12,16 +10,11 @@ Version: **0.7.1**
|
|
|
12
10
|
|
|
13
11
|
- Ephemeral ECDH handshake (`prime256v1`)
|
|
14
12
|
- AES-256-GCM encrypted envelopes
|
|
15
|
-
-
|
|
16
|
-
- Secure room routing (`join`, `leave`, `leaveAll`, `to(room).emit`)
|
|
17
|
-
- Heartbeat and zombie socket cleanup
|
|
18
|
-
- Auto-reconnect with fresh re-handshake
|
|
19
|
-
- **Binary payload support**: `Buffer`, `Uint8Array`, `Blob`
|
|
20
|
-
- **Server middleware pipeline** via `SecureServer.use(...)`
|
|
13
|
+
- ACK request/response (Promise + callback styles)
|
|
14
|
+
- Secure room routing (`join`, `leave`, `leaveAll`, `to(room).emit(...)`)
|
|
21
15
|
- Middleware phases: `connection`, `incoming`, `outgoing`
|
|
22
|
-
-
|
|
23
|
-
- **
|
|
24
|
-
- Optional MCP bridge package: `@aegis-fluxion/mcp-adapter`
|
|
16
|
+
- Rate limiting and DDoS controls per connection and IP
|
|
17
|
+
- **Horizontal scaling hooks** via pluggable `SecureServerAdapter`
|
|
25
18
|
|
|
26
19
|
---
|
|
27
20
|
|
|
@@ -33,84 +26,126 @@ npm install @aegis-fluxion/core ws
|
|
|
33
26
|
|
|
34
27
|
---
|
|
35
28
|
|
|
36
|
-
##
|
|
29
|
+
## SecureServer adapter API (horizontal scaling)
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
normalization.
|
|
31
|
+
### Core types
|
|
40
32
|
|
|
41
33
|
```ts
|
|
42
|
-
|
|
34
|
+
export interface SecureServerAdapterMessage {
|
|
35
|
+
version: 1;
|
|
36
|
+
originServerId: string;
|
|
37
|
+
scope: "broadcast" | "room";
|
|
38
|
+
event: string;
|
|
39
|
+
data: unknown;
|
|
40
|
+
emittedAt: number;
|
|
41
|
+
room?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SecureServerAdapter {
|
|
45
|
+
attach(server: SecureServer): void | Promise<void>;
|
|
46
|
+
publish(message: SecureServerAdapterMessage): void | Promise<void>;
|
|
47
|
+
detach?(server: SecureServer): void | Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
```
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
### SecureServer hooks
|
|
45
52
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
- constructor option: `new SecureServer({ ..., adapter })`
|
|
54
|
+
- runtime binding: `await server.setAdapter(adapter)`
|
|
55
|
+
- inbound relay: `await server.handleAdapterMessage(message)`
|
|
56
|
+
- instance identity: `server.serverId`
|
|
57
|
+
|
|
58
|
+
### Message normalization helper
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { normalizeSecureServerAdapterMessage } from "@aegis-fluxion/core";
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Use it in adapters before delivering inbound Pub/Sub payloads to `SecureServer`.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Adapter integration example
|
|
50
69
|
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
SecureServer,
|
|
73
|
+
type SecureServerAdapter,
|
|
74
|
+
type SecureServerAdapterMessage
|
|
75
|
+
} from "@aegis-fluxion/core";
|
|
76
|
+
|
|
77
|
+
class InMemoryAdapter implements SecureServerAdapter {
|
|
78
|
+
private static readonly peers = new Set<InMemoryAdapter>();
|
|
79
|
+
private server: SecureServer | null = null;
|
|
80
|
+
|
|
81
|
+
async attach(server: SecureServer): Promise<void> {
|
|
82
|
+
this.server = server;
|
|
83
|
+
InMemoryAdapter.peers.add(this);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async publish(message: SecureServerAdapterMessage): Promise<void> {
|
|
87
|
+
for (const peer of InMemoryAdapter.peers) {
|
|
88
|
+
if (peer === this || !peer.server) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await peer.server.handleAdapterMessage(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async detach(server: SecureServer): Promise<void> {
|
|
97
|
+
if (this.server !== server) {
|
|
98
|
+
return;
|
|
53
99
|
}
|
|
54
100
|
|
|
55
|
-
|
|
101
|
+
this.server = null;
|
|
102
|
+
InMemoryAdapter.peers.delete(this);
|
|
56
103
|
}
|
|
104
|
+
}
|
|
57
105
|
|
|
58
|
-
|
|
106
|
+
const server = new SecureServer({
|
|
107
|
+
host: "127.0.0.1",
|
|
108
|
+
port: 8080,
|
|
109
|
+
adapter: new InMemoryAdapter()
|
|
59
110
|
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Middleware and ACK example
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { SecureClient, SecureServer } from "@aegis-fluxion/core";
|
|
119
|
+
|
|
120
|
+
const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
|
|
60
121
|
|
|
61
122
|
server.use(async (context, next) => {
|
|
62
|
-
if (
|
|
63
|
-
context.
|
|
64
|
-
context.event === "post:create" &&
|
|
65
|
-
typeof context.data === "object" &&
|
|
66
|
-
context.data !== null
|
|
67
|
-
) {
|
|
68
|
-
const payload = context.data as { title?: string };
|
|
69
|
-
context.data = { title: String(payload.title ?? "").trim() };
|
|
123
|
+
if (context.phase === "connection") {
|
|
124
|
+
context.metadata.set("auth.role", "operator");
|
|
70
125
|
}
|
|
71
126
|
|
|
72
127
|
await next();
|
|
73
|
-
|
|
74
|
-
if (context.phase === "outgoing" && context.event === "post:create") {
|
|
75
|
-
context.data = {
|
|
76
|
-
...(context.data as Record<string, unknown>),
|
|
77
|
-
middleware: true
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
128
|
});
|
|
81
129
|
|
|
82
|
-
server.on("
|
|
130
|
+
server.on("jobs:create", async (payload, client) => {
|
|
83
131
|
return {
|
|
84
132
|
ok: true,
|
|
85
|
-
role: client.metadata.get("role"),
|
|
133
|
+
role: client.metadata.get("auth.role"),
|
|
86
134
|
payload
|
|
87
135
|
};
|
|
88
136
|
});
|
|
89
137
|
|
|
90
|
-
const client = new SecureClient("ws://127.0.0.1:8080"
|
|
91
|
-
wsOptions: {
|
|
92
|
-
headers: {
|
|
93
|
-
"x-api-key": "dev-secret"
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
138
|
+
const client = new SecureClient("ws://127.0.0.1:8080");
|
|
97
139
|
|
|
98
140
|
client.on("ready", async () => {
|
|
99
|
-
const response = await client.emit("
|
|
141
|
+
const response = await client.emit("jobs:create", { id: "job-42" }, { timeoutMs: 1200 });
|
|
100
142
|
console.log(response);
|
|
101
143
|
});
|
|
102
144
|
```
|
|
103
145
|
|
|
104
|
-
Notes:
|
|
105
|
-
|
|
106
|
-
- Throwing in `connection` middleware rejects the socket and closes with code `1008`.
|
|
107
|
-
- `metadata` is mutable in middleware (`Map`) and exposed read-only on `SecureServerClient`.
|
|
108
|
-
|
|
109
146
|
---
|
|
110
147
|
|
|
111
|
-
## Rate
|
|
112
|
-
|
|
113
|
-
`SecureServer` can enforce burst limits per connection and per source IP before event handlers run.
|
|
148
|
+
## Rate limiting and DDoS shield
|
|
114
149
|
|
|
115
150
|
```ts
|
|
116
151
|
import { SecureServer } from "@aegis-fluxion/core";
|
|
@@ -123,7 +158,7 @@ const server = new SecureServer({
|
|
|
123
158
|
windowMs: 1_000,
|
|
124
159
|
maxEventsPerConnection: 120,
|
|
125
160
|
maxEventsPerIp: 300,
|
|
126
|
-
action: "throttle",
|
|
161
|
+
action: "throttle", // or "disconnect"
|
|
127
162
|
throttleMs: 150,
|
|
128
163
|
maxThrottleMs: 2_000,
|
|
129
164
|
disconnectAfterViolations: 4,
|
|
@@ -133,199 +168,17 @@ const server = new SecureServer({
|
|
|
133
168
|
});
|
|
134
169
|
```
|
|
135
170
|
|
|
136
|
-
Behavior summary:
|
|
137
|
-
|
|
138
|
-
- When limits are exceeded, the server can **throttle** or **disconnect** the peer.
|
|
139
|
-
- Throttle mode delays the first over-limit message and drops subsequent flood packets during the throttle window.
|
|
140
|
-
- Disconnect mode closes abusive sockets with your configured close code/reason.
|
|
141
|
-
- Source IP is resolved from `x-forwarded-for` (first hop) or socket remote address.
|
|
142
|
-
|
|
143
171
|
---
|
|
144
172
|
|
|
145
|
-
##
|
|
173
|
+
## Binary payload support
|
|
146
174
|
|
|
147
|
-
|
|
148
|
-
transport.
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
import { SecureClient, SecureServer } from "@aegis-fluxion/core";
|
|
152
|
-
import { SecureMCPTransport } from "@aegis-fluxion/mcp-adapter";
|
|
153
|
-
|
|
154
|
-
const secureServer = new SecureServer({ host: "127.0.0.1", port: 9091 });
|
|
155
|
-
|
|
156
|
-
secureServer.on("connection", async (client) => {
|
|
157
|
-
const mcpServerTransport = new SecureMCPTransport({
|
|
158
|
-
mode: "server",
|
|
159
|
-
server: secureServer,
|
|
160
|
-
clientId: client.id
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
mcpServerTransport.onmessage = async (message) => {
|
|
164
|
-
// Forward into your MCP server runtime.
|
|
165
|
-
console.log("MCP request on server tunnel", message);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
await mcpServerTransport.start();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const secureClient = new SecureClient("ws://127.0.0.1:9091");
|
|
172
|
-
|
|
173
|
-
const mcpClientTransport = new SecureMCPTransport({
|
|
174
|
-
mode: "client",
|
|
175
|
-
client: secureClient
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
await mcpClientTransport.start();
|
|
179
|
-
|
|
180
|
-
await mcpClientTransport.send({
|
|
181
|
-
jsonrpc: "2.0",
|
|
182
|
-
id: 100,
|
|
183
|
-
method: "tools/list",
|
|
184
|
-
params: {}
|
|
185
|
-
});
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Binary Data Support
|
|
191
|
-
|
|
192
|
-
`@aegis-fluxion/core` supports encrypted binary payload transfer while preserving type fidelity.
|
|
193
|
-
|
|
194
|
-
Supported send/receive types:
|
|
175
|
+
Supported encrypted payload types:
|
|
195
176
|
|
|
196
177
|
- `Buffer`
|
|
197
178
|
- `Uint8Array`
|
|
198
179
|
- `Blob`
|
|
199
180
|
|
|
200
|
-
Binary
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
## Example: Encrypted Binary Event
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
import { SecureClient, SecureServer } from "@aegis-fluxion/core";
|
|
208
|
-
|
|
209
|
-
const server = new SecureServer({ host: "127.0.0.1", port: 8080 });
|
|
210
|
-
const client = new SecureClient("ws://127.0.0.1:8080");
|
|
211
|
-
|
|
212
|
-
server.on("image:chunk", (data, socket) => {
|
|
213
|
-
const chunk = data as Buffer;
|
|
214
|
-
|
|
215
|
-
if (!Buffer.isBuffer(chunk)) {
|
|
216
|
-
throw new Error("Expected Buffer payload.");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
socket.emit("image:chunk:ack", chunk);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
client.on("ready", () => {
|
|
223
|
-
const imageChunk = Buffer.from("89504e470d0a", "hex");
|
|
224
|
-
client.emit("image:chunk", imageChunk);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
client.on("image:chunk:ack", (payload) => {
|
|
228
|
-
const echoedChunk = payload as Buffer;
|
|
229
|
-
console.log("Echoed bytes:", echoedChunk.byteLength);
|
|
230
|
-
});
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## Example: ACK Roundtrip with Mixed Binary Types
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
server.on("binary:inspect", async (payload) => {
|
|
239
|
-
const { file, bytes, blob } = payload as {
|
|
240
|
-
file: Buffer;
|
|
241
|
-
bytes: Uint8Array;
|
|
242
|
-
blob: Blob;
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
fileBytes: file.byteLength,
|
|
247
|
-
bytesBytes: bytes.byteLength,
|
|
248
|
-
blobBytes: blob.size
|
|
249
|
-
};
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
client.on("ready", async () => {
|
|
253
|
-
const result = await client.emit(
|
|
254
|
-
"binary:inspect",
|
|
255
|
-
{
|
|
256
|
-
file: Buffer.from("file-binary"),
|
|
257
|
-
bytes: Uint8Array.from([1, 2, 3, 4]),
|
|
258
|
-
blob: new Blob([Buffer.from("blob-binary")], {
|
|
259
|
-
type: "application/octet-stream"
|
|
260
|
-
})
|
|
261
|
-
},
|
|
262
|
-
{ timeoutMs: 1500 }
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
console.log(result);
|
|
266
|
-
});
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## API Snapshot
|
|
272
|
-
|
|
273
|
-
### `SecureServer`
|
|
274
|
-
|
|
275
|
-
- `on("connection" | "ready" | "disconnect" | "error", handler)`
|
|
276
|
-
- `on("custom:event", (data, client) => unknown | Promise<unknown>)`
|
|
277
|
-
- `use((context, next) => void | Promise<void>)`
|
|
278
|
-
- `emit(event, data): SecureServer`
|
|
279
|
-
- `emitTo(clientId, event, data): boolean`
|
|
280
|
-
- `emitTo(clientId, event, data, callback): boolean`
|
|
281
|
-
- `emitTo(clientId, event, data, options): Promise<unknown>`
|
|
282
|
-
- `emitTo(clientId, event, data, options, callback): boolean`
|
|
283
|
-
- `to(room).emit(event, data): SecureServer`
|
|
284
|
-
- `close(code?, reason?): void`
|
|
285
|
-
|
|
286
|
-
### `SecureServerClient`
|
|
287
|
-
|
|
288
|
-
- `id: string`
|
|
289
|
-
- `socket: WebSocket`
|
|
290
|
-
- `metadata: ReadonlyMap<string, unknown>`
|
|
291
|
-
- `emit(event, data, ...ackArgs): boolean | Promise<unknown>`
|
|
292
|
-
- `join(room): boolean`
|
|
293
|
-
- `leave(room): boolean`
|
|
294
|
-
- `leaveAll(): number`
|
|
295
|
-
|
|
296
|
-
### Middleware Types
|
|
297
|
-
|
|
298
|
-
- `SecureServerMiddleware`
|
|
299
|
-
- `SecureServerMiddlewareContext`
|
|
300
|
-
- `SecureServerConnectionMiddlewareContext`
|
|
301
|
-
- `SecureServerMessageMiddlewareContext`
|
|
302
|
-
- `SecureServerMiddlewareNext`
|
|
303
|
-
- `SecureServerRateLimitOptions`
|
|
304
|
-
- `SecureServerRateLimitAction`
|
|
305
|
-
|
|
306
|
-
### `SecureClient`
|
|
307
|
-
|
|
308
|
-
- `connect(): void`
|
|
309
|
-
- `disconnect(code?, reason?): void`
|
|
310
|
-
- `isConnected(): boolean`
|
|
311
|
-
- `readyState: number | null`
|
|
312
|
-
- `emit(event, data): boolean`
|
|
313
|
-
- `emit(event, data, callback): boolean`
|
|
314
|
-
- `emit(event, data, options): Promise<unknown>`
|
|
315
|
-
- `emit(event, data, options, callback): boolean`
|
|
316
|
-
- `on("connect" | "ready" | "disconnect" | "error", handler)`
|
|
317
|
-
- `on("custom:event", handler)`
|
|
318
|
-
|
|
319
|
-
---
|
|
320
|
-
|
|
321
|
-
## Security Notes
|
|
322
|
-
|
|
323
|
-
- All payloads (including binary) are encrypted end-to-end with AES-256-GCM.
|
|
324
|
-
- Authentication tags are verified on every packet (tampered packets are dropped).
|
|
325
|
-
- Internal transport events are reserved (`__handshake`, `__rpc:req`, `__rpc:res`).
|
|
326
|
-
- Pending ACK requests are rejected on timeout/disconnect.
|
|
327
|
-
- Overload traffic can be throttled or disconnected before custom handlers are invoked.
|
|
328
|
-
- Middleware-level policy rejection uses WebSocket close code `1008`.
|
|
181
|
+
Binary fields can be nested in standard JSON objects and arrays.
|
|
329
182
|
|
|
330
183
|
---
|
|
331
184
|
|
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,51 @@ var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
|
|
|
40
40
|
var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
|
|
41
41
|
var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
|
|
42
42
|
var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
|
|
43
|
+
var SECURE_SERVER_ADAPTER_MESSAGE_VERSION = 1;
|
|
44
|
+
function normalizeSecureServerAdapterMessage(value) {
|
|
45
|
+
if (!isPlainObject(value)) {
|
|
46
|
+
throw new Error("SecureServer adapter message must be a plain object.");
|
|
47
|
+
}
|
|
48
|
+
if (value.version !== SECURE_SERVER_ADAPTER_MESSAGE_VERSION) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unsupported SecureServer adapter message version: ${String(value.version)}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.originServerId !== "string" || value.originServerId.trim().length === 0) {
|
|
54
|
+
throw new Error("SecureServer adapter message originServerId must be a non-empty string.");
|
|
55
|
+
}
|
|
56
|
+
if (value.scope !== "broadcast" && value.scope !== "room") {
|
|
57
|
+
throw new Error('SecureServer adapter message scope must be either "broadcast" or "room".');
|
|
58
|
+
}
|
|
59
|
+
if (typeof value.event !== "string" || value.event.trim().length === 0) {
|
|
60
|
+
throw new Error("SecureServer adapter message event must be a non-empty string.");
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.emittedAt !== "number" || !Number.isFinite(value.emittedAt)) {
|
|
63
|
+
throw new Error("SecureServer adapter message emittedAt must be a finite number.");
|
|
64
|
+
}
|
|
65
|
+
if (value.scope === "room") {
|
|
66
|
+
if (typeof value.room !== "string" || value.room.trim().length === 0) {
|
|
67
|
+
throw new Error("SecureServer adapter message room must be a non-empty string.");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
71
|
+
originServerId: value.originServerId,
|
|
72
|
+
scope: value.scope,
|
|
73
|
+
event: value.event,
|
|
74
|
+
data: value.data,
|
|
75
|
+
emittedAt: value.emittedAt,
|
|
76
|
+
room: value.room.trim()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
81
|
+
originServerId: value.originServerId,
|
|
82
|
+
scope: value.scope,
|
|
83
|
+
event: value.event,
|
|
84
|
+
data: value.data,
|
|
85
|
+
emittedAt: value.emittedAt
|
|
86
|
+
};
|
|
87
|
+
}
|
|
43
88
|
function normalizeToError(error, fallbackMessage) {
|
|
44
89
|
if (error instanceof Error) {
|
|
45
90
|
return error;
|
|
@@ -368,7 +413,9 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
|
|
|
368
413
|
return plaintext.toString("utf8");
|
|
369
414
|
}
|
|
370
415
|
var SecureServer = class {
|
|
416
|
+
instanceId = crypto.randomUUID();
|
|
371
417
|
socketServer;
|
|
418
|
+
adapter = null;
|
|
372
419
|
heartbeatConfig;
|
|
373
420
|
rateLimitConfig;
|
|
374
421
|
heartbeatIntervalHandle = null;
|
|
@@ -393,19 +440,76 @@ var SecureServer = class {
|
|
|
393
440
|
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
394
441
|
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
395
442
|
constructor(options) {
|
|
396
|
-
const { heartbeat, rateLimit, ...socketServerOptions } = options;
|
|
443
|
+
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
397
444
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
398
445
|
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
399
446
|
this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
|
|
400
447
|
this.bindSocketServerEvents();
|
|
401
448
|
this.startHeartbeatLoop();
|
|
449
|
+
if (adapter) {
|
|
450
|
+
void this.setAdapter(adapter).catch(() => {
|
|
451
|
+
return void 0;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
402
454
|
}
|
|
403
455
|
get clientCount() {
|
|
404
456
|
return this.clientsById.size;
|
|
405
457
|
}
|
|
458
|
+
get serverId() {
|
|
459
|
+
return this.instanceId;
|
|
460
|
+
}
|
|
406
461
|
get clients() {
|
|
407
462
|
return this.clientsById;
|
|
408
463
|
}
|
|
464
|
+
async setAdapter(adapter) {
|
|
465
|
+
const previousAdapter = this.adapter;
|
|
466
|
+
if (previousAdapter === adapter) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
if (previousAdapter?.detach) {
|
|
471
|
+
await Promise.resolve(previousAdapter.detach(this));
|
|
472
|
+
}
|
|
473
|
+
this.adapter = null;
|
|
474
|
+
if (!adapter) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
await Promise.resolve(adapter.attach(this));
|
|
478
|
+
this.adapter = adapter;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const normalizedError = normalizeToError(
|
|
481
|
+
error,
|
|
482
|
+
"Failed to set SecureServer adapter."
|
|
483
|
+
);
|
|
484
|
+
this.notifyError(normalizedError);
|
|
485
|
+
throw normalizedError;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async handleAdapterMessage(message) {
|
|
489
|
+
try {
|
|
490
|
+
const normalizedMessage = normalizeSecureServerAdapterMessage(message);
|
|
491
|
+
if (normalizedMessage.originServerId === this.instanceId) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (normalizedMessage.scope === "broadcast") {
|
|
495
|
+
this.emitLocally(normalizedMessage.event, normalizedMessage.data);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!normalizedMessage.room) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.emitToRoom(
|
|
502
|
+
normalizedMessage.room,
|
|
503
|
+
normalizedMessage.event,
|
|
504
|
+
normalizedMessage.data,
|
|
505
|
+
false
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
this.notifyError(
|
|
509
|
+
normalizeToError(error, "Failed to process SecureServer adapter message.")
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
409
513
|
on(event, handler) {
|
|
410
514
|
try {
|
|
411
515
|
if (event === "connection") {
|
|
@@ -492,12 +596,12 @@ var SecureServer = class {
|
|
|
492
596
|
if (isReservedEmitEvent(event)) {
|
|
493
597
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
494
598
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
599
|
+
this.emitLocally(event, data);
|
|
600
|
+
this.publishAdapterMessage({
|
|
601
|
+
scope: "broadcast",
|
|
602
|
+
event,
|
|
603
|
+
data
|
|
604
|
+
});
|
|
501
605
|
} catch (error) {
|
|
502
606
|
this.notifyError(normalizeToError(error, "Failed to emit server event."));
|
|
503
607
|
}
|
|
@@ -554,7 +658,7 @@ var SecureServer = class {
|
|
|
554
658
|
return {
|
|
555
659
|
emit: (event, data) => {
|
|
556
660
|
try {
|
|
557
|
-
this.emitToRoom(normalizedRoom, event, data);
|
|
661
|
+
this.emitToRoom(normalizedRoom, event, data, true);
|
|
558
662
|
} catch (error) {
|
|
559
663
|
this.notifyError(
|
|
560
664
|
normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
|
|
@@ -567,6 +671,15 @@ var SecureServer = class {
|
|
|
567
671
|
close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
|
|
568
672
|
try {
|
|
569
673
|
this.stopHeartbeatLoop();
|
|
674
|
+
const activeAdapter = this.adapter;
|
|
675
|
+
this.adapter = null;
|
|
676
|
+
if (activeAdapter?.detach) {
|
|
677
|
+
void Promise.resolve(activeAdapter.detach(this)).catch((error) => {
|
|
678
|
+
this.notifyError(
|
|
679
|
+
normalizeToError(error, "Failed to detach SecureServer adapter during close.")
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
570
683
|
for (const client of this.clientsById.values()) {
|
|
571
684
|
this.rejectPendingRpcRequests(
|
|
572
685
|
client.socket,
|
|
@@ -1435,6 +1548,48 @@ var SecureServer = class {
|
|
|
1435
1548
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
1436
1549
|
};
|
|
1437
1550
|
}
|
|
1551
|
+
emitLocally(event, data) {
|
|
1552
|
+
const envelope = { event, data };
|
|
1553
|
+
for (const client of this.clientsById.values()) {
|
|
1554
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1555
|
+
return void 0;
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
publishAdapterMessage(message) {
|
|
1560
|
+
if (!this.adapter) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
let adapterMessage;
|
|
1564
|
+
if (message.scope === "room") {
|
|
1565
|
+
if (!message.room) {
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
adapterMessage = {
|
|
1569
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1570
|
+
originServerId: this.instanceId,
|
|
1571
|
+
scope: "room",
|
|
1572
|
+
event: message.event,
|
|
1573
|
+
data: message.data,
|
|
1574
|
+
emittedAt: Date.now(),
|
|
1575
|
+
room: message.room
|
|
1576
|
+
};
|
|
1577
|
+
} else {
|
|
1578
|
+
adapterMessage = {
|
|
1579
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1580
|
+
originServerId: this.instanceId,
|
|
1581
|
+
scope: "broadcast",
|
|
1582
|
+
event: message.event,
|
|
1583
|
+
data: message.data,
|
|
1584
|
+
emittedAt: Date.now()
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
void Promise.resolve(this.adapter.publish(adapterMessage)).catch((error) => {
|
|
1588
|
+
this.notifyError(
|
|
1589
|
+
normalizeToError(error, "Failed to publish SecureServer adapter message.")
|
|
1590
|
+
);
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1438
1593
|
normalizeRoomName(room) {
|
|
1439
1594
|
if (typeof room !== "string") {
|
|
1440
1595
|
throw new Error("Room name must be a string.");
|
|
@@ -1500,22 +1655,29 @@ var SecureServer = class {
|
|
|
1500
1655
|
}
|
|
1501
1656
|
return roomNames.length;
|
|
1502
1657
|
}
|
|
1503
|
-
emitToRoom(room, event, data) {
|
|
1658
|
+
emitToRoom(room, event, data, replicate) {
|
|
1504
1659
|
if (isReservedEmitEvent(event)) {
|
|
1505
1660
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1506
1661
|
}
|
|
1507
1662
|
const roomMembers = this.roomMembersByName.get(room);
|
|
1508
|
-
if (
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1663
|
+
if (roomMembers && roomMembers.size > 0) {
|
|
1664
|
+
const envelope = { event, data };
|
|
1665
|
+
for (const clientId of roomMembers) {
|
|
1666
|
+
const client = this.clientsById.get(clientId);
|
|
1667
|
+
if (!client) {
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1671
|
+
return void 0;
|
|
1672
|
+
});
|
|
1516
1673
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1674
|
+
}
|
|
1675
|
+
if (replicate) {
|
|
1676
|
+
this.publishAdapterMessage({
|
|
1677
|
+
scope: "room",
|
|
1678
|
+
room,
|
|
1679
|
+
event,
|
|
1680
|
+
data
|
|
1519
1681
|
});
|
|
1520
1682
|
}
|
|
1521
1683
|
}
|
|
@@ -2120,5 +2282,6 @@ var SecureClient = class {
|
|
|
2120
2282
|
|
|
2121
2283
|
exports.SecureClient = SecureClient;
|
|
2122
2284
|
exports.SecureServer = SecureServer;
|
|
2285
|
+
exports.normalizeSecureServerAdapterMessage = normalizeSecureServerAdapterMessage;
|
|
2123
2286
|
//# sourceMappingURL=index.cjs.map
|
|
2124
2287
|
//# sourceMappingURL=index.cjs.map
|