@aegis-fluxion/core 0.8.0 → 0.10.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/README.md +195 -2
- package/dist/index.cjs +867 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -1
- package/dist/index.d.ts +80 -1
- package/dist/index.js +867 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
|
+
var stream = require('stream');
|
|
4
5
|
var WebSocket = require('ws');
|
|
5
6
|
|
|
6
7
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -16,6 +17,7 @@ var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
|
16
17
|
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
17
18
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
18
19
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
20
|
+
var INTERNAL_STREAM_FRAME_EVENT = "__stream:frame";
|
|
19
21
|
var READY_EVENT = "ready";
|
|
20
22
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
21
23
|
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
@@ -33,6 +35,9 @@ var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
|
33
35
|
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
34
36
|
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
35
37
|
var DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE = 1e4;
|
|
38
|
+
var STREAM_FRAME_VERSION = 1;
|
|
39
|
+
var DEFAULT_STREAM_CHUNK_SIZE_BYTES = 64 * 1024;
|
|
40
|
+
var MAX_STREAM_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
36
41
|
var RESUMPTION_NONCE_LENGTH = 16;
|
|
37
42
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
38
43
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
@@ -254,8 +259,11 @@ function parseEnvelopeFromText(decodedPayload) {
|
|
|
254
259
|
function decodeCloseReason(reason) {
|
|
255
260
|
return reason.toString("utf8");
|
|
256
261
|
}
|
|
262
|
+
function escapePrometheusLabelValue(value) {
|
|
263
|
+
return value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/"/g, '\\"');
|
|
264
|
+
}
|
|
257
265
|
function isReservedEmitEvent(event) {
|
|
258
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
266
|
+
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === INTERNAL_STREAM_FRAME_EVENT || event === READY_EVENT;
|
|
259
267
|
}
|
|
260
268
|
function isPromiseLike(value) {
|
|
261
269
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -267,6 +275,240 @@ function normalizeRpcTimeout(timeoutMs) {
|
|
|
267
275
|
}
|
|
268
276
|
return resolvedTimeoutMs;
|
|
269
277
|
}
|
|
278
|
+
function normalizeStreamChunkSize(chunkSizeBytes) {
|
|
279
|
+
const resolvedChunkSize = chunkSizeBytes ?? DEFAULT_STREAM_CHUNK_SIZE_BYTES;
|
|
280
|
+
if (!Number.isInteger(resolvedChunkSize) || resolvedChunkSize <= 0) {
|
|
281
|
+
throw new Error("Stream chunkSizeBytes must be a positive integer.");
|
|
282
|
+
}
|
|
283
|
+
if (resolvedChunkSize > MAX_STREAM_CHUNK_SIZE_BYTES) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Stream chunkSizeBytes cannot exceed ${MAX_STREAM_CHUNK_SIZE_BYTES} bytes.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return resolvedChunkSize;
|
|
289
|
+
}
|
|
290
|
+
function resolveKnownStreamSourceSize(source, hint) {
|
|
291
|
+
if (hint !== void 0) {
|
|
292
|
+
if (!Number.isInteger(hint) || hint < 0) {
|
|
293
|
+
throw new Error("Stream totalBytes must be a non-negative integer.");
|
|
294
|
+
}
|
|
295
|
+
return hint;
|
|
296
|
+
}
|
|
297
|
+
if (Buffer.isBuffer(source)) {
|
|
298
|
+
return source.length;
|
|
299
|
+
}
|
|
300
|
+
if (source instanceof Uint8Array) {
|
|
301
|
+
return source.byteLength;
|
|
302
|
+
}
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
function normalizeChunkSourceValue(value) {
|
|
306
|
+
if (Buffer.isBuffer(value)) {
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
if (value instanceof Uint8Array) {
|
|
310
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
311
|
+
}
|
|
312
|
+
if (value instanceof ArrayBuffer) {
|
|
313
|
+
return Buffer.from(value);
|
|
314
|
+
}
|
|
315
|
+
if (typeof value === "string") {
|
|
316
|
+
return Buffer.from(value, "utf8");
|
|
317
|
+
}
|
|
318
|
+
throw new Error("Stream source yielded an unsupported chunk value.");
|
|
319
|
+
}
|
|
320
|
+
function isAsyncIterableValue(value) {
|
|
321
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
322
|
+
}
|
|
323
|
+
function isReadableSource(value) {
|
|
324
|
+
return value instanceof stream.Readable;
|
|
325
|
+
}
|
|
326
|
+
function splitChunkBuffer(chunk, chunkSizeBytes) {
|
|
327
|
+
if (chunk.length <= chunkSizeBytes) {
|
|
328
|
+
return [chunk];
|
|
329
|
+
}
|
|
330
|
+
const splitChunks = [];
|
|
331
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSizeBytes) {
|
|
332
|
+
splitChunks.push(chunk.subarray(offset, offset + chunkSizeBytes));
|
|
333
|
+
}
|
|
334
|
+
return splitChunks;
|
|
335
|
+
}
|
|
336
|
+
async function* createChunkStreamIterator(source, chunkSizeBytes) {
|
|
337
|
+
if (Buffer.isBuffer(source)) {
|
|
338
|
+
yield* splitChunkBuffer(source, chunkSizeBytes);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (source instanceof Uint8Array) {
|
|
342
|
+
yield* splitChunkBuffer(
|
|
343
|
+
Buffer.from(source.buffer, source.byteOffset, source.byteLength),
|
|
344
|
+
chunkSizeBytes
|
|
345
|
+
);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (isReadableSource(source) || isAsyncIterableValue(source)) {
|
|
349
|
+
for await (const chunkValue of source) {
|
|
350
|
+
const normalizedChunk = normalizeChunkSourceValue(chunkValue);
|
|
351
|
+
if (normalizedChunk.length === 0) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
yield* splitChunkBuffer(normalizedChunk, chunkSizeBytes);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
throw new Error("Unsupported stream source type.");
|
|
359
|
+
}
|
|
360
|
+
function parseStreamFramePayload(data) {
|
|
361
|
+
if (typeof data !== "object" || data === null) {
|
|
362
|
+
throw new Error("Invalid stream frame payload format.");
|
|
363
|
+
}
|
|
364
|
+
const payload = data;
|
|
365
|
+
if (payload.version !== STREAM_FRAME_VERSION) {
|
|
366
|
+
throw new Error(`Unsupported stream frame version: ${String(payload.version)}.`);
|
|
367
|
+
}
|
|
368
|
+
if (typeof payload.streamId !== "string" || payload.streamId.trim().length === 0) {
|
|
369
|
+
throw new Error("Stream frame streamId must be a non-empty string.");
|
|
370
|
+
}
|
|
371
|
+
if (payload.type === "start") {
|
|
372
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
373
|
+
throw new Error("Stream start frame event must be a non-empty string.");
|
|
374
|
+
}
|
|
375
|
+
if (payload.totalBytes !== void 0 && (!Number.isInteger(payload.totalBytes) || payload.totalBytes < 0)) {
|
|
376
|
+
throw new Error("Stream start frame totalBytes must be a non-negative integer.");
|
|
377
|
+
}
|
|
378
|
+
if (payload.metadata !== void 0 && !isPlainObject(payload.metadata)) {
|
|
379
|
+
throw new Error("Stream start frame metadata must be a plain object when provided.");
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
version: STREAM_FRAME_VERSION,
|
|
383
|
+
type: "start",
|
|
384
|
+
streamId: payload.streamId.trim(),
|
|
385
|
+
event: payload.event.trim(),
|
|
386
|
+
...payload.metadata ? { metadata: payload.metadata } : {},
|
|
387
|
+
...payload.totalBytes !== void 0 ? { totalBytes: payload.totalBytes } : {}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (payload.type === "chunk") {
|
|
391
|
+
const { index, byteLength } = payload;
|
|
392
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) {
|
|
393
|
+
throw new Error("Stream chunk frame index must be a non-negative integer.");
|
|
394
|
+
}
|
|
395
|
+
if (typeof payload.payload !== "string" || payload.payload.length === 0) {
|
|
396
|
+
throw new Error("Stream chunk frame payload must be a non-empty base64 string.");
|
|
397
|
+
}
|
|
398
|
+
if (typeof byteLength !== "number" || !Number.isInteger(byteLength) || byteLength <= 0) {
|
|
399
|
+
throw new Error("Stream chunk frame byteLength must be a positive integer.");
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
version: STREAM_FRAME_VERSION,
|
|
403
|
+
type: "chunk",
|
|
404
|
+
streamId: payload.streamId.trim(),
|
|
405
|
+
index,
|
|
406
|
+
payload: payload.payload,
|
|
407
|
+
byteLength
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (payload.type === "end") {
|
|
411
|
+
const { chunkCount, totalBytes } = payload;
|
|
412
|
+
if (typeof chunkCount !== "number" || !Number.isInteger(chunkCount) || chunkCount < 0) {
|
|
413
|
+
throw new Error("Stream end frame chunkCount must be a non-negative integer.");
|
|
414
|
+
}
|
|
415
|
+
if (typeof totalBytes !== "number" || !Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
416
|
+
throw new Error("Stream end frame totalBytes must be a non-negative integer.");
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
version: STREAM_FRAME_VERSION,
|
|
420
|
+
type: "end",
|
|
421
|
+
streamId: payload.streamId.trim(),
|
|
422
|
+
chunkCount,
|
|
423
|
+
totalBytes
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (payload.type === "abort") {
|
|
427
|
+
if (typeof payload.reason !== "string" || payload.reason.trim().length === 0) {
|
|
428
|
+
throw new Error("Stream abort frame reason must be a non-empty string.");
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
version: STREAM_FRAME_VERSION,
|
|
432
|
+
type: "abort",
|
|
433
|
+
streamId: payload.streamId.trim(),
|
|
434
|
+
reason: payload.reason.trim()
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new Error("Unsupported stream frame type.");
|
|
438
|
+
}
|
|
439
|
+
async function transmitChunkedStreamFrames(event, source, options, sendFrame) {
|
|
440
|
+
const chunkSizeBytes = normalizeStreamChunkSize(options?.chunkSizeBytes);
|
|
441
|
+
const totalBytesHint = resolveKnownStreamSourceSize(source, options?.totalBytes);
|
|
442
|
+
if (options?.metadata !== void 0 && !isPlainObject(options.metadata)) {
|
|
443
|
+
throw new Error("Stream metadata must be a plain object when provided.");
|
|
444
|
+
}
|
|
445
|
+
if (options?.signal?.aborted) {
|
|
446
|
+
throw new Error("Stream transfer aborted before dispatch.");
|
|
447
|
+
}
|
|
448
|
+
const streamId = crypto.randomUUID();
|
|
449
|
+
let chunkCount = 0;
|
|
450
|
+
let totalBytes = 0;
|
|
451
|
+
await sendFrame({
|
|
452
|
+
version: STREAM_FRAME_VERSION,
|
|
453
|
+
type: "start",
|
|
454
|
+
streamId,
|
|
455
|
+
event,
|
|
456
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
457
|
+
...totalBytesHint !== void 0 ? { totalBytes: totalBytesHint } : {}
|
|
458
|
+
});
|
|
459
|
+
try {
|
|
460
|
+
for await (const chunkBuffer of createChunkStreamIterator(source, chunkSizeBytes)) {
|
|
461
|
+
if (options?.signal?.aborted) {
|
|
462
|
+
throw new Error("Stream transfer aborted by caller signal.");
|
|
463
|
+
}
|
|
464
|
+
if (chunkBuffer.length === 0) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
await sendFrame({
|
|
468
|
+
version: STREAM_FRAME_VERSION,
|
|
469
|
+
type: "chunk",
|
|
470
|
+
streamId,
|
|
471
|
+
index: chunkCount,
|
|
472
|
+
payload: chunkBuffer.toString("base64"),
|
|
473
|
+
byteLength: chunkBuffer.length
|
|
474
|
+
});
|
|
475
|
+
chunkCount += 1;
|
|
476
|
+
totalBytes += chunkBuffer.length;
|
|
477
|
+
}
|
|
478
|
+
if (totalBytesHint !== void 0 && totalBytes !== totalBytesHint) {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`Stream totalBytes mismatch. Expected ${totalBytesHint}, received ${totalBytes}.`
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
await sendFrame({
|
|
484
|
+
version: STREAM_FRAME_VERSION,
|
|
485
|
+
type: "end",
|
|
486
|
+
streamId,
|
|
487
|
+
chunkCount,
|
|
488
|
+
totalBytes
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
streamId,
|
|
492
|
+
chunkCount,
|
|
493
|
+
totalBytes
|
|
494
|
+
};
|
|
495
|
+
} catch (error) {
|
|
496
|
+
const normalizedError = normalizeToError(
|
|
497
|
+
error,
|
|
498
|
+
`Chunked stream transfer failed for event "${event}".`
|
|
499
|
+
);
|
|
500
|
+
try {
|
|
501
|
+
await sendFrame({
|
|
502
|
+
version: STREAM_FRAME_VERSION,
|
|
503
|
+
type: "abort",
|
|
504
|
+
streamId,
|
|
505
|
+
reason: normalizedError.message
|
|
506
|
+
});
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
throw normalizedError;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
270
512
|
function parseRpcRequestPayload(data) {
|
|
271
513
|
if (typeof data !== "object" || data === null) {
|
|
272
514
|
throw new Error("Invalid RPC request payload format.");
|
|
@@ -554,6 +796,7 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
|
|
|
554
796
|
}
|
|
555
797
|
var SecureServer = class {
|
|
556
798
|
instanceId = crypto.randomUUID();
|
|
799
|
+
startedAtMs = Date.now();
|
|
557
800
|
socketServer;
|
|
558
801
|
adapter = null;
|
|
559
802
|
heartbeatConfig;
|
|
@@ -563,6 +806,7 @@ var SecureServer = class {
|
|
|
563
806
|
clientsById = /* @__PURE__ */ new Map();
|
|
564
807
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
565
808
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
809
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
566
810
|
connectionHandlers = /* @__PURE__ */ new Set();
|
|
567
811
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
568
812
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -573,6 +817,7 @@ var SecureServer = class {
|
|
|
573
817
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
574
818
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
575
819
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
820
|
+
incomingStreamsBySocket = /* @__PURE__ */ new WeakMap();
|
|
576
821
|
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
577
822
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
578
823
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
@@ -581,6 +826,20 @@ var SecureServer = class {
|
|
|
581
826
|
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
582
827
|
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
583
828
|
sessionTicketStore = /* @__PURE__ */ new Map();
|
|
829
|
+
telemetryCounters = {
|
|
830
|
+
totalConnections: 0,
|
|
831
|
+
handshakeSuccessTotal: 0,
|
|
832
|
+
handshakeFailureTotal: 0,
|
|
833
|
+
resumeHandshakeSuccessTotal: 0,
|
|
834
|
+
resumeHandshakeFailureTotal: 0,
|
|
835
|
+
encryptedMessagesSentTotal: 0,
|
|
836
|
+
encryptedMessagesReceivedTotal: 0,
|
|
837
|
+
encryptedBytesSentTotal: 0,
|
|
838
|
+
encryptedBytesReceivedTotal: 0,
|
|
839
|
+
ddosBlockedTotal: 0,
|
|
840
|
+
ddosThrottledTotal: 0,
|
|
841
|
+
ddosDisconnectedTotal: 0
|
|
842
|
+
};
|
|
584
843
|
constructor(options) {
|
|
585
844
|
const { heartbeat, rateLimit, sessionResumption, adapter, ...socketServerOptions } = options;
|
|
586
845
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
@@ -604,6 +863,79 @@ var SecureServer = class {
|
|
|
604
863
|
get clients() {
|
|
605
864
|
return this.clientsById;
|
|
606
865
|
}
|
|
866
|
+
getMetrics() {
|
|
867
|
+
const now = Date.now();
|
|
868
|
+
const uptimeSeconds = Math.max(0, (now - this.startedAtMs) / 1e3);
|
|
869
|
+
return {
|
|
870
|
+
serverId: this.instanceId,
|
|
871
|
+
timestampMs: now,
|
|
872
|
+
uptimeSeconds,
|
|
873
|
+
activeConnections: this.clientCount,
|
|
874
|
+
totalConnections: this.telemetryCounters.totalConnections,
|
|
875
|
+
handshakeSuccessTotal: this.telemetryCounters.handshakeSuccessTotal,
|
|
876
|
+
handshakeFailureTotal: this.telemetryCounters.handshakeFailureTotal,
|
|
877
|
+
resumeHandshakeSuccessTotal: this.telemetryCounters.resumeHandshakeSuccessTotal,
|
|
878
|
+
resumeHandshakeFailureTotal: this.telemetryCounters.resumeHandshakeFailureTotal,
|
|
879
|
+
encryptedMessagesSentTotal: this.telemetryCounters.encryptedMessagesSentTotal,
|
|
880
|
+
encryptedMessagesReceivedTotal: this.telemetryCounters.encryptedMessagesReceivedTotal,
|
|
881
|
+
encryptedBytesSentTotal: this.telemetryCounters.encryptedBytesSentTotal,
|
|
882
|
+
encryptedBytesReceivedTotal: this.telemetryCounters.encryptedBytesReceivedTotal,
|
|
883
|
+
ddosBlockedTotal: this.telemetryCounters.ddosBlockedTotal,
|
|
884
|
+
ddosThrottledTotal: this.telemetryCounters.ddosThrottledTotal,
|
|
885
|
+
ddosDisconnectedTotal: this.telemetryCounters.ddosDisconnectedTotal
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
getMetricsPrometheus() {
|
|
889
|
+
const metrics = this.getMetrics();
|
|
890
|
+
const labelValue = escapePrometheusLabelValue(metrics.serverId);
|
|
891
|
+
const labels = `{server_id="${labelValue}"}`;
|
|
892
|
+
const lines = [
|
|
893
|
+
"# HELP aegis_fluxion_server_active_connections Number of currently active secure connections.",
|
|
894
|
+
"# TYPE aegis_fluxion_server_active_connections gauge",
|
|
895
|
+
`aegis_fluxion_server_active_connections${labels} ${metrics.activeConnections}`,
|
|
896
|
+
"# HELP aegis_fluxion_server_total_connections_total Total number of accepted secure connections since process start.",
|
|
897
|
+
"# TYPE aegis_fluxion_server_total_connections_total counter",
|
|
898
|
+
`aegis_fluxion_server_total_connections_total${labels} ${metrics.totalConnections}`,
|
|
899
|
+
"# HELP aegis_fluxion_server_uptime_seconds Process uptime in seconds.",
|
|
900
|
+
"# TYPE aegis_fluxion_server_uptime_seconds gauge",
|
|
901
|
+
`aegis_fluxion_server_uptime_seconds${labels} ${metrics.uptimeSeconds}`,
|
|
902
|
+
"# HELP aegis_fluxion_server_handshake_success_total Total successful secure handshakes.",
|
|
903
|
+
"# TYPE aegis_fluxion_server_handshake_success_total counter",
|
|
904
|
+
`aegis_fluxion_server_handshake_success_total${labels} ${metrics.handshakeSuccessTotal}`,
|
|
905
|
+
"# HELP aegis_fluxion_server_handshake_failure_total Total failed secure handshake attempts.",
|
|
906
|
+
"# TYPE aegis_fluxion_server_handshake_failure_total counter",
|
|
907
|
+
`aegis_fluxion_server_handshake_failure_total${labels} ${metrics.handshakeFailureTotal}`,
|
|
908
|
+
"# HELP aegis_fluxion_server_resume_handshake_success_total Total successful session-resume handshakes.",
|
|
909
|
+
"# TYPE aegis_fluxion_server_resume_handshake_success_total counter",
|
|
910
|
+
`aegis_fluxion_server_resume_handshake_success_total${labels} ${metrics.resumeHandshakeSuccessTotal}`,
|
|
911
|
+
"# HELP aegis_fluxion_server_resume_handshake_failure_total Total failed session-resume handshakes.",
|
|
912
|
+
"# TYPE aegis_fluxion_server_resume_handshake_failure_total counter",
|
|
913
|
+
`aegis_fluxion_server_resume_handshake_failure_total${labels} ${metrics.resumeHandshakeFailureTotal}`,
|
|
914
|
+
"# HELP aegis_fluxion_server_encrypted_messages_sent_total Total encrypted messages sent by the server.",
|
|
915
|
+
"# TYPE aegis_fluxion_server_encrypted_messages_sent_total counter",
|
|
916
|
+
`aegis_fluxion_server_encrypted_messages_sent_total${labels} ${metrics.encryptedMessagesSentTotal}`,
|
|
917
|
+
"# HELP aegis_fluxion_server_encrypted_messages_received_total Total encrypted messages received by the server.",
|
|
918
|
+
"# TYPE aegis_fluxion_server_encrypted_messages_received_total counter",
|
|
919
|
+
`aegis_fluxion_server_encrypted_messages_received_total${labels} ${metrics.encryptedMessagesReceivedTotal}`,
|
|
920
|
+
"# HELP aegis_fluxion_server_encrypted_bytes_sent_total Total encrypted bytes sent by the server.",
|
|
921
|
+
"# TYPE aegis_fluxion_server_encrypted_bytes_sent_total counter",
|
|
922
|
+
`aegis_fluxion_server_encrypted_bytes_sent_total${labels} ${metrics.encryptedBytesSentTotal}`,
|
|
923
|
+
"# HELP aegis_fluxion_server_encrypted_bytes_received_total Total encrypted bytes received by the server.",
|
|
924
|
+
"# TYPE aegis_fluxion_server_encrypted_bytes_received_total counter",
|
|
925
|
+
`aegis_fluxion_server_encrypted_bytes_received_total${labels} ${metrics.encryptedBytesReceivedTotal}`,
|
|
926
|
+
"# HELP aegis_fluxion_server_ddos_blocked_total Total DDoS/flood attempts blocked by rate limiting.",
|
|
927
|
+
"# TYPE aegis_fluxion_server_ddos_blocked_total counter",
|
|
928
|
+
`aegis_fluxion_server_ddos_blocked_total${labels} ${metrics.ddosBlockedTotal}`,
|
|
929
|
+
"# HELP aegis_fluxion_server_ddos_throttled_total Total requests slowed down by adaptive throttling.",
|
|
930
|
+
"# TYPE aegis_fluxion_server_ddos_throttled_total counter",
|
|
931
|
+
`aegis_fluxion_server_ddos_throttled_total${labels} ${metrics.ddosThrottledTotal}`,
|
|
932
|
+
"# HELP aegis_fluxion_server_ddos_disconnected_total Total sockets disconnected due to severe rate limit violations.",
|
|
933
|
+
"# TYPE aegis_fluxion_server_ddos_disconnected_total counter",
|
|
934
|
+
`aegis_fluxion_server_ddos_disconnected_total${labels} ${metrics.ddosDisconnectedTotal}`
|
|
935
|
+
];
|
|
936
|
+
return `${lines.join("\n")}
|
|
937
|
+
`;
|
|
938
|
+
}
|
|
607
939
|
async setAdapter(adapter) {
|
|
608
940
|
const previousAdapter = this.adapter;
|
|
609
941
|
if (previousAdapter === adapter) {
|
|
@@ -721,6 +1053,38 @@ var SecureServer = class {
|
|
|
721
1053
|
}
|
|
722
1054
|
return this;
|
|
723
1055
|
}
|
|
1056
|
+
onStream(event, handler) {
|
|
1057
|
+
try {
|
|
1058
|
+
if (isReservedEmitEvent(event)) {
|
|
1059
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
1060
|
+
}
|
|
1061
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
1062
|
+
listeners.add(handler);
|
|
1063
|
+
this.streamEventHandlers.set(event, listeners);
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
this.notifyError(
|
|
1066
|
+
normalizeToError(error, "Failed to register server stream handler.")
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
return this;
|
|
1070
|
+
}
|
|
1071
|
+
offStream(event, handler) {
|
|
1072
|
+
try {
|
|
1073
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
1074
|
+
if (!listeners) {
|
|
1075
|
+
return this;
|
|
1076
|
+
}
|
|
1077
|
+
listeners.delete(handler);
|
|
1078
|
+
if (listeners.size === 0) {
|
|
1079
|
+
this.streamEventHandlers.delete(event);
|
|
1080
|
+
}
|
|
1081
|
+
} catch (error) {
|
|
1082
|
+
this.notifyError(
|
|
1083
|
+
normalizeToError(error, "Failed to remove server stream handler.")
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return this;
|
|
1087
|
+
}
|
|
724
1088
|
use(middleware) {
|
|
725
1089
|
try {
|
|
726
1090
|
if (typeof middleware !== "function") {
|
|
@@ -796,6 +1160,40 @@ var SecureServer = class {
|
|
|
796
1160
|
return false;
|
|
797
1161
|
}
|
|
798
1162
|
}
|
|
1163
|
+
async emitStreamTo(clientId, event, source, options) {
|
|
1164
|
+
try {
|
|
1165
|
+
if (isReservedEmitEvent(event)) {
|
|
1166
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1167
|
+
}
|
|
1168
|
+
const client = this.clientsById.get(clientId);
|
|
1169
|
+
if (!client) {
|
|
1170
|
+
throw new Error(`Client with id ${clientId} was not found.`);
|
|
1171
|
+
}
|
|
1172
|
+
if (!this.isClientHandshakeReady(client.socket)) {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`Cannot stream event "${event}" before secure handshake completion for client ${client.id}.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
return await transmitChunkedStreamFrames(
|
|
1178
|
+
event,
|
|
1179
|
+
source,
|
|
1180
|
+
options,
|
|
1181
|
+
async (framePayload) => {
|
|
1182
|
+
await this.sendEncryptedEnvelope(client.socket, {
|
|
1183
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
1184
|
+
data: framePayload
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
);
|
|
1188
|
+
} catch (error) {
|
|
1189
|
+
const normalizedError = normalizeToError(
|
|
1190
|
+
error,
|
|
1191
|
+
`Failed to emit chunked stream event "${event}" to client ${clientId}.`
|
|
1192
|
+
);
|
|
1193
|
+
this.notifyError(normalizedError);
|
|
1194
|
+
throw normalizedError;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
799
1197
|
to(room) {
|
|
800
1198
|
const normalizedRoom = this.normalizeRoomName(room);
|
|
801
1199
|
return {
|
|
@@ -828,6 +1226,10 @@ var SecureServer = class {
|
|
|
828
1226
|
client.socket,
|
|
829
1227
|
new Error("Server closed before ACK response was received.")
|
|
830
1228
|
);
|
|
1229
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1230
|
+
client.socket,
|
|
1231
|
+
"Server closed before stream transfer completed."
|
|
1232
|
+
);
|
|
831
1233
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
832
1234
|
if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
|
|
833
1235
|
client.socket.close(code, reason);
|
|
@@ -842,6 +1244,35 @@ var SecureServer = class {
|
|
|
842
1244
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
843
1245
|
}
|
|
844
1246
|
}
|
|
1247
|
+
recordHandshakeSuccess(resumed) {
|
|
1248
|
+
this.telemetryCounters.handshakeSuccessTotal += 1;
|
|
1249
|
+
if (resumed) {
|
|
1250
|
+
this.telemetryCounters.resumeHandshakeSuccessTotal += 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
recordHandshakeFailure(resumed) {
|
|
1254
|
+
this.telemetryCounters.handshakeFailureTotal += 1;
|
|
1255
|
+
if (resumed) {
|
|
1256
|
+
this.telemetryCounters.resumeHandshakeFailureTotal += 1;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
recordEncryptedMessageSent(byteLength) {
|
|
1260
|
+
this.telemetryCounters.encryptedMessagesSentTotal += 1;
|
|
1261
|
+
this.telemetryCounters.encryptedBytesSentTotal += Math.max(0, byteLength);
|
|
1262
|
+
}
|
|
1263
|
+
recordEncryptedMessageReceived(byteLength) {
|
|
1264
|
+
this.telemetryCounters.encryptedMessagesReceivedTotal += 1;
|
|
1265
|
+
this.telemetryCounters.encryptedBytesReceivedTotal += Math.max(0, byteLength);
|
|
1266
|
+
}
|
|
1267
|
+
recordDdosBlocked(disconnected) {
|
|
1268
|
+
this.telemetryCounters.ddosBlockedTotal += 1;
|
|
1269
|
+
if (disconnected) {
|
|
1270
|
+
this.telemetryCounters.ddosDisconnectedTotal += 1;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
recordDdosThrottled() {
|
|
1274
|
+
this.telemetryCounters.ddosThrottledTotal += 1;
|
|
1275
|
+
}
|
|
845
1276
|
resolveHeartbeatConfig(heartbeatOptions) {
|
|
846
1277
|
const intervalMs = heartbeatOptions?.intervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
847
1278
|
const timeoutMs = heartbeatOptions?.timeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
@@ -1271,6 +1702,7 @@ var SecureServer = class {
|
|
|
1271
1702
|
lastPingAt: 0
|
|
1272
1703
|
});
|
|
1273
1704
|
this.roomNamesByClientId.set(clientId, /* @__PURE__ */ new Set());
|
|
1705
|
+
this.telemetryCounters.totalConnections += 1;
|
|
1274
1706
|
socket.on("message", (rawData) => {
|
|
1275
1707
|
void this.handleIncomingMessage(client, rawData);
|
|
1276
1708
|
});
|
|
@@ -1299,6 +1731,7 @@ var SecureServer = class {
|
|
|
1299
1731
|
try {
|
|
1300
1732
|
const rateLimitDecision = this.evaluateIncomingRateLimit(client);
|
|
1301
1733
|
if (rateLimitDecision.shouldDisconnect) {
|
|
1734
|
+
this.recordDdosBlocked(true);
|
|
1302
1735
|
this.notifyError(
|
|
1303
1736
|
new Error(
|
|
1304
1737
|
`Rate limit disconnect triggered for client ${client.id}.`
|
|
@@ -1313,9 +1746,11 @@ var SecureServer = class {
|
|
|
1313
1746
|
return;
|
|
1314
1747
|
}
|
|
1315
1748
|
if (rateLimitDecision.shouldDrop) {
|
|
1749
|
+
this.recordDdosBlocked(false);
|
|
1316
1750
|
return;
|
|
1317
1751
|
}
|
|
1318
1752
|
if (rateLimitDecision.throttleDelayMs > 0) {
|
|
1753
|
+
this.recordDdosThrottled();
|
|
1319
1754
|
this.notifyError(
|
|
1320
1755
|
new Error(
|
|
1321
1756
|
`Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
|
|
@@ -1356,6 +1791,7 @@ var SecureServer = class {
|
|
|
1356
1791
|
return;
|
|
1357
1792
|
}
|
|
1358
1793
|
let decryptedPayload;
|
|
1794
|
+
const encryptedPayloadByteLength = rawDataToBuffer(rawData).length;
|
|
1359
1795
|
try {
|
|
1360
1796
|
decryptedPayload = decryptSerializedEnvelope(rawData, encryptionKey);
|
|
1361
1797
|
} catch {
|
|
@@ -1363,6 +1799,7 @@ var SecureServer = class {
|
|
|
1363
1799
|
return;
|
|
1364
1800
|
}
|
|
1365
1801
|
const decryptedEnvelope = parseEnvelopeFromText(decryptedPayload);
|
|
1802
|
+
this.recordEncryptedMessageReceived(encryptedPayloadByteLength);
|
|
1366
1803
|
if (decryptedEnvelope.event === INTERNAL_RPC_RESPONSE_EVENT) {
|
|
1367
1804
|
this.handleRpcResponse(client.socket, decryptedEnvelope.data);
|
|
1368
1805
|
return;
|
|
@@ -1371,6 +1808,10 @@ var SecureServer = class {
|
|
|
1371
1808
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1372
1809
|
return;
|
|
1373
1810
|
}
|
|
1811
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
1812
|
+
this.handleIncomingStreamFrame(client, decryptedEnvelope.data);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1374
1815
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1375
1816
|
this.notifyError(
|
|
1376
1817
|
new Error(
|
|
@@ -1412,6 +1853,10 @@ var SecureServer = class {
|
|
|
1412
1853
|
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
1413
1854
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
1414
1855
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
1856
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1857
|
+
client.socket,
|
|
1858
|
+
`Client ${client.id} disconnected before stream transfer completed.`
|
|
1859
|
+
);
|
|
1415
1860
|
const decodedReason = decodeCloseReason(reason);
|
|
1416
1861
|
for (const handler of this.disconnectHandlers) {
|
|
1417
1862
|
try {
|
|
@@ -1457,6 +1902,197 @@ var SecureServer = class {
|
|
|
1457
1902
|
}
|
|
1458
1903
|
}
|
|
1459
1904
|
}
|
|
1905
|
+
getOrCreateIncomingServerStreams(socket) {
|
|
1906
|
+
const existingStreams = this.incomingStreamsBySocket.get(socket);
|
|
1907
|
+
if (existingStreams) {
|
|
1908
|
+
return existingStreams;
|
|
1909
|
+
}
|
|
1910
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
1911
|
+
this.incomingStreamsBySocket.set(socket, streamMap);
|
|
1912
|
+
return streamMap;
|
|
1913
|
+
}
|
|
1914
|
+
cleanupIncomingStreamsForSocket(socket, reason) {
|
|
1915
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1916
|
+
if (!streamMap) {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
for (const streamState of streamMap.values()) {
|
|
1920
|
+
streamState.stream.destroy(new Error(reason));
|
|
1921
|
+
}
|
|
1922
|
+
streamMap.clear();
|
|
1923
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1924
|
+
}
|
|
1925
|
+
abortIncomingServerStream(socket, streamId, reason) {
|
|
1926
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1927
|
+
if (!streamMap) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const streamState = streamMap.get(streamId);
|
|
1931
|
+
if (!streamState) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
streamState.stream.destroy(new Error(reason));
|
|
1935
|
+
streamMap.delete(streamId);
|
|
1936
|
+
if (streamMap.size === 0) {
|
|
1937
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
dispatchServerStreamEvent(event, stream, info, client) {
|
|
1941
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
1942
|
+
if (!handlers || handlers.size === 0) {
|
|
1943
|
+
stream.resume();
|
|
1944
|
+
this.notifyError(
|
|
1945
|
+
new Error(
|
|
1946
|
+
`No stream handler is registered for event "${event}" on server client ${client.id}.`
|
|
1947
|
+
)
|
|
1948
|
+
);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
for (const handler of handlers) {
|
|
1952
|
+
try {
|
|
1953
|
+
const handlerResult = handler(stream, info, client);
|
|
1954
|
+
if (isPromiseLike(handlerResult)) {
|
|
1955
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1956
|
+
this.notifyError(
|
|
1957
|
+
normalizeToError(
|
|
1958
|
+
error,
|
|
1959
|
+
`Server stream handler failed for event ${event}.`
|
|
1960
|
+
)
|
|
1961
|
+
);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
this.notifyError(
|
|
1966
|
+
normalizeToError(
|
|
1967
|
+
error,
|
|
1968
|
+
`Server stream handler failed for event ${event}.`
|
|
1969
|
+
)
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
handleIncomingStreamStartFrame(client, framePayload) {
|
|
1975
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
1976
|
+
throw new Error(
|
|
1977
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
const incomingStreams = this.getOrCreateIncomingServerStreams(client.socket);
|
|
1981
|
+
if (incomingStreams.has(framePayload.streamId)) {
|
|
1982
|
+
throw new Error(
|
|
1983
|
+
`Stream ${framePayload.streamId} already exists for client ${client.id}.`
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
const stream$1 = new stream.PassThrough();
|
|
1987
|
+
const streamInfo = {
|
|
1988
|
+
streamId: framePayload.streamId,
|
|
1989
|
+
event: framePayload.event,
|
|
1990
|
+
startedAt: Date.now(),
|
|
1991
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
1992
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
1993
|
+
};
|
|
1994
|
+
incomingStreams.set(framePayload.streamId, {
|
|
1995
|
+
info: streamInfo,
|
|
1996
|
+
stream: stream$1,
|
|
1997
|
+
expectedChunkIndex: 0,
|
|
1998
|
+
receivedBytes: 0
|
|
1999
|
+
});
|
|
2000
|
+
this.dispatchServerStreamEvent(framePayload.event, stream$1, streamInfo, client);
|
|
2001
|
+
}
|
|
2002
|
+
handleIncomingStreamChunkFrame(client, framePayload) {
|
|
2003
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
2004
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
2005
|
+
if (!incomingStreams || !streamState) {
|
|
2006
|
+
throw new Error(
|
|
2007
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
2011
|
+
throw new Error(
|
|
2012
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
2016
|
+
framePayload.payload,
|
|
2017
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
2018
|
+
);
|
|
2019
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
2020
|
+
throw new Error(
|
|
2021
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
streamState.expectedChunkIndex += 1;
|
|
2025
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
2026
|
+
streamState.stream.write(chunkBuffer);
|
|
2027
|
+
}
|
|
2028
|
+
handleIncomingStreamEndFrame(client, framePayload) {
|
|
2029
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
2030
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
2031
|
+
if (!incomingStreams || !streamState) {
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
2037
|
+
throw new Error(
|
|
2038
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
2047
|
+
throw new Error(
|
|
2048
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
streamState.stream.end();
|
|
2052
|
+
incomingStreams.delete(framePayload.streamId);
|
|
2053
|
+
if (incomingStreams.size === 0) {
|
|
2054
|
+
this.incomingStreamsBySocket.delete(client.socket);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
handleIncomingStreamAbortFrame(client, framePayload) {
|
|
2058
|
+
this.abortIncomingServerStream(
|
|
2059
|
+
client.socket,
|
|
2060
|
+
framePayload.streamId,
|
|
2061
|
+
framePayload.reason
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
handleIncomingStreamFrame(client, data) {
|
|
2065
|
+
let framePayload = null;
|
|
2066
|
+
try {
|
|
2067
|
+
framePayload = parseStreamFramePayload(data);
|
|
2068
|
+
if (framePayload.type === "start") {
|
|
2069
|
+
this.handleIncomingStreamStartFrame(client, framePayload);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (framePayload.type === "chunk") {
|
|
2073
|
+
this.handleIncomingStreamChunkFrame(client, framePayload);
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (framePayload.type === "end") {
|
|
2077
|
+
this.handleIncomingStreamEndFrame(client, framePayload);
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
this.handleIncomingStreamAbortFrame(client, framePayload);
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
const normalizedError = normalizeToError(
|
|
2083
|
+
error,
|
|
2084
|
+
`Failed to process incoming stream frame for client ${client.id}.`
|
|
2085
|
+
);
|
|
2086
|
+
if (framePayload) {
|
|
2087
|
+
this.abortIncomingServerStream(
|
|
2088
|
+
client.socket,
|
|
2089
|
+
framePayload.streamId,
|
|
2090
|
+
normalizedError.message
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
this.notifyError(normalizedError);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
1460
2096
|
async executeServerMiddleware(context) {
|
|
1461
2097
|
if (this.middlewareHandlers.length === 0) {
|
|
1462
2098
|
return;
|
|
@@ -1522,6 +2158,7 @@ var SecureServer = class {
|
|
|
1522
2158
|
try {
|
|
1523
2159
|
const serializedEnvelope = await serializeEnvelope(envelope.event, envelope.data);
|
|
1524
2160
|
const encryptedPayload = encryptSerializedEnvelope(serializedEnvelope, encryptionKey);
|
|
2161
|
+
this.recordEncryptedMessageSent(encryptedPayload.length);
|
|
1525
2162
|
socket.send(encryptedPayload);
|
|
1526
2163
|
} catch (error) {
|
|
1527
2164
|
const normalizedError = normalizeToError(error, "Failed to send encrypted server payload.");
|
|
@@ -1720,6 +2357,7 @@ var SecureServer = class {
|
|
|
1720
2357
|
}
|
|
1721
2358
|
handleResumeHandshake(client, payload) {
|
|
1722
2359
|
if (!this.sessionResumptionConfig.enabled) {
|
|
2360
|
+
this.recordHandshakeFailure(true);
|
|
1723
2361
|
this.sendResumeAck(client.socket, {
|
|
1724
2362
|
ok: false,
|
|
1725
2363
|
reason: "Session resumption is disabled."
|
|
@@ -1728,6 +2366,7 @@ var SecureServer = class {
|
|
|
1728
2366
|
}
|
|
1729
2367
|
const ticketRecord = this.getSessionTicket(payload.sessionId);
|
|
1730
2368
|
if (!ticketRecord) {
|
|
2369
|
+
this.recordHandshakeFailure(true);
|
|
1731
2370
|
this.sendResumeAck(client.socket, {
|
|
1732
2371
|
ok: false,
|
|
1733
2372
|
reason: "Session ticket is unknown or expired."
|
|
@@ -1754,6 +2393,7 @@ var SecureServer = class {
|
|
|
1754
2393
|
clientNonce
|
|
1755
2394
|
);
|
|
1756
2395
|
if (!equalsConstantTime(receivedProof, expectedProof)) {
|
|
2396
|
+
this.recordHandshakeFailure(true);
|
|
1757
2397
|
this.sendResumeAck(client.socket, {
|
|
1758
2398
|
ok: false,
|
|
1759
2399
|
reason: "Session resumption proof validation failed."
|
|
@@ -1774,6 +2414,7 @@ var SecureServer = class {
|
|
|
1774
2414
|
this.sharedSecretBySocket.set(client.socket, resumedKey);
|
|
1775
2415
|
this.encryptionKeyBySocket.set(client.socket, resumedKey);
|
|
1776
2416
|
handshakeState.isReady = true;
|
|
2417
|
+
this.recordHandshakeSuccess(true);
|
|
1777
2418
|
this.sendResumeAck(client.socket, {
|
|
1778
2419
|
ok: true,
|
|
1779
2420
|
sessionId: ticketRecord.sessionId,
|
|
@@ -1783,6 +2424,7 @@ var SecureServer = class {
|
|
|
1783
2424
|
this.notifyReady(client);
|
|
1784
2425
|
this.issueSessionTicket(client.socket, resumedKey);
|
|
1785
2426
|
} catch (error) {
|
|
2427
|
+
this.recordHandshakeFailure(true);
|
|
1786
2428
|
this.sendResumeAck(client.socket, {
|
|
1787
2429
|
ok: false,
|
|
1788
2430
|
reason: "Session resumption payload was invalid."
|
|
@@ -1813,10 +2455,12 @@ var SecureServer = class {
|
|
|
1813
2455
|
this.sharedSecretBySocket.set(client.socket, sharedSecret);
|
|
1814
2456
|
this.encryptionKeyBySocket.set(client.socket, encryptionKey);
|
|
1815
2457
|
handshakeState.isReady = true;
|
|
2458
|
+
this.recordHandshakeSuccess(false);
|
|
1816
2459
|
void this.flushQueuedPayloads(client.socket);
|
|
1817
2460
|
this.notifyReady(client);
|
|
1818
2461
|
this.issueSessionTicket(client.socket, encryptionKey);
|
|
1819
2462
|
} catch (error) {
|
|
2463
|
+
this.recordHandshakeFailure(false);
|
|
1820
2464
|
this.notifyError(normalizeToError(error, "Failed to complete server handshake."));
|
|
1821
2465
|
}
|
|
1822
2466
|
}
|
|
@@ -1885,6 +2529,9 @@ var SecureServer = class {
|
|
|
1885
2529
|
}
|
|
1886
2530
|
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
1887
2531
|
},
|
|
2532
|
+
emitStream: (event, source, options) => {
|
|
2533
|
+
return this.emitStreamTo(clientId, event, source, options);
|
|
2534
|
+
},
|
|
1888
2535
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
1889
2536
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
1890
2537
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -2045,6 +2692,7 @@ var SecureClient = class {
|
|
|
2045
2692
|
reconnectTimer = null;
|
|
2046
2693
|
isManualDisconnectRequested = false;
|
|
2047
2694
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
2695
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
2048
2696
|
connectHandlers = /* @__PURE__ */ new Set();
|
|
2049
2697
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
2050
2698
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -2052,6 +2700,7 @@ var SecureClient = class {
|
|
|
2052
2700
|
handshakeState = null;
|
|
2053
2701
|
pendingPayloadQueue = [];
|
|
2054
2702
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2703
|
+
incomingStreams = /* @__PURE__ */ new Map();
|
|
2055
2704
|
sessionTicket = null;
|
|
2056
2705
|
get readyState() {
|
|
2057
2706
|
return this.socket?.readyState ?? null;
|
|
@@ -2161,6 +2810,38 @@ var SecureClient = class {
|
|
|
2161
2810
|
}
|
|
2162
2811
|
return this;
|
|
2163
2812
|
}
|
|
2813
|
+
onStream(event, handler) {
|
|
2814
|
+
try {
|
|
2815
|
+
if (isReservedEmitEvent(event)) {
|
|
2816
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
2817
|
+
}
|
|
2818
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
2819
|
+
listeners.add(handler);
|
|
2820
|
+
this.streamEventHandlers.set(event, listeners);
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
this.notifyError(
|
|
2823
|
+
normalizeToError(error, "Failed to register client stream handler.")
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
return this;
|
|
2827
|
+
}
|
|
2828
|
+
offStream(event, handler) {
|
|
2829
|
+
try {
|
|
2830
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
2831
|
+
if (!listeners) {
|
|
2832
|
+
return this;
|
|
2833
|
+
}
|
|
2834
|
+
listeners.delete(handler);
|
|
2835
|
+
if (listeners.size === 0) {
|
|
2836
|
+
this.streamEventHandlers.delete(event);
|
|
2837
|
+
}
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
this.notifyError(
|
|
2840
|
+
normalizeToError(error, "Failed to remove client stream handler.")
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
return this;
|
|
2844
|
+
}
|
|
2164
2845
|
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
2165
2846
|
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
2166
2847
|
try {
|
|
@@ -2206,6 +2887,39 @@ var SecureClient = class {
|
|
|
2206
2887
|
return false;
|
|
2207
2888
|
}
|
|
2208
2889
|
}
|
|
2890
|
+
async emitStream(event, source, options) {
|
|
2891
|
+
try {
|
|
2892
|
+
if (isReservedEmitEvent(event)) {
|
|
2893
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
2894
|
+
}
|
|
2895
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
2896
|
+
throw new Error("Client socket is not connected.");
|
|
2897
|
+
}
|
|
2898
|
+
if (!this.isHandshakeReady()) {
|
|
2899
|
+
throw new Error(
|
|
2900
|
+
`Cannot stream event "${event}" before secure handshake completion.`
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
return await transmitChunkedStreamFrames(
|
|
2904
|
+
event,
|
|
2905
|
+
source,
|
|
2906
|
+
options,
|
|
2907
|
+
async (framePayload) => {
|
|
2908
|
+
await this.sendEncryptedEnvelope({
|
|
2909
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
2910
|
+
data: framePayload
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
);
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
const normalizedError = normalizeToError(
|
|
2916
|
+
error,
|
|
2917
|
+
`Failed to emit chunked stream event "${event}".`
|
|
2918
|
+
);
|
|
2919
|
+
this.notifyError(normalizedError);
|
|
2920
|
+
throw normalizedError;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2209
2923
|
resolveReconnectConfig(reconnectOptions) {
|
|
2210
2924
|
if (typeof reconnectOptions === "boolean") {
|
|
2211
2925
|
return {
|
|
@@ -2369,6 +3083,10 @@ var SecureClient = class {
|
|
|
2369
3083
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2370
3084
|
return;
|
|
2371
3085
|
}
|
|
3086
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
3087
|
+
this.handleIncomingStreamFrame(decryptedEnvelope.data);
|
|
3088
|
+
return;
|
|
3089
|
+
}
|
|
2372
3090
|
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2373
3091
|
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2374
3092
|
return;
|
|
@@ -2383,6 +3101,9 @@ var SecureClient = class {
|
|
|
2383
3101
|
this.socket = null;
|
|
2384
3102
|
this.handshakeState = null;
|
|
2385
3103
|
this.pendingPayloadQueue = [];
|
|
3104
|
+
this.cleanupIncomingStreams(
|
|
3105
|
+
"Client disconnected before stream transfer completed."
|
|
3106
|
+
);
|
|
2386
3107
|
this.rejectPendingRpcRequests(
|
|
2387
3108
|
new Error("Client disconnected before ACK response was received.")
|
|
2388
3109
|
);
|
|
@@ -2429,6 +3150,151 @@ var SecureClient = class {
|
|
|
2429
3150
|
}
|
|
2430
3151
|
}
|
|
2431
3152
|
}
|
|
3153
|
+
cleanupIncomingStreams(reason) {
|
|
3154
|
+
for (const streamState of this.incomingStreams.values()) {
|
|
3155
|
+
streamState.stream.destroy(new Error(reason));
|
|
3156
|
+
}
|
|
3157
|
+
this.incomingStreams.clear();
|
|
3158
|
+
}
|
|
3159
|
+
abortIncomingClientStream(streamId, reason) {
|
|
3160
|
+
const streamState = this.incomingStreams.get(streamId);
|
|
3161
|
+
if (!streamState) {
|
|
3162
|
+
return;
|
|
3163
|
+
}
|
|
3164
|
+
streamState.stream.destroy(new Error(reason));
|
|
3165
|
+
this.incomingStreams.delete(streamId);
|
|
3166
|
+
}
|
|
3167
|
+
dispatchClientStreamEvent(event, stream, info) {
|
|
3168
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
3169
|
+
if (!handlers || handlers.size === 0) {
|
|
3170
|
+
stream.resume();
|
|
3171
|
+
this.notifyError(
|
|
3172
|
+
new Error(`No stream handler is registered for event "${event}" on client.`)
|
|
3173
|
+
);
|
|
3174
|
+
return;
|
|
3175
|
+
}
|
|
3176
|
+
for (const handler of handlers) {
|
|
3177
|
+
try {
|
|
3178
|
+
const handlerResult = handler(stream, info);
|
|
3179
|
+
if (isPromiseLike(handlerResult)) {
|
|
3180
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
3181
|
+
this.notifyError(
|
|
3182
|
+
normalizeToError(
|
|
3183
|
+
error,
|
|
3184
|
+
`Client stream handler failed for event ${event}.`
|
|
3185
|
+
)
|
|
3186
|
+
);
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
} catch (error) {
|
|
3190
|
+
this.notifyError(
|
|
3191
|
+
normalizeToError(error, `Client stream handler failed for event ${event}.`)
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
handleIncomingClientStreamStartFrame(framePayload) {
|
|
3197
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
3198
|
+
throw new Error(
|
|
3199
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
if (this.incomingStreams.has(framePayload.streamId)) {
|
|
3203
|
+
throw new Error(`Stream ${framePayload.streamId} already exists on client.`);
|
|
3204
|
+
}
|
|
3205
|
+
const stream$1 = new stream.PassThrough();
|
|
3206
|
+
const streamInfo = {
|
|
3207
|
+
streamId: framePayload.streamId,
|
|
3208
|
+
event: framePayload.event,
|
|
3209
|
+
startedAt: Date.now(),
|
|
3210
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
3211
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
3212
|
+
};
|
|
3213
|
+
this.incomingStreams.set(framePayload.streamId, {
|
|
3214
|
+
info: streamInfo,
|
|
3215
|
+
stream: stream$1,
|
|
3216
|
+
expectedChunkIndex: 0,
|
|
3217
|
+
receivedBytes: 0
|
|
3218
|
+
});
|
|
3219
|
+
this.dispatchClientStreamEvent(framePayload.event, stream$1, streamInfo);
|
|
3220
|
+
}
|
|
3221
|
+
handleIncomingClientStreamChunkFrame(framePayload) {
|
|
3222
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3223
|
+
if (!streamState) {
|
|
3224
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3225
|
+
}
|
|
3226
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
3227
|
+
throw new Error(
|
|
3228
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
3232
|
+
framePayload.payload,
|
|
3233
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
3234
|
+
);
|
|
3235
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
3236
|
+
throw new Error(
|
|
3237
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
3238
|
+
);
|
|
3239
|
+
}
|
|
3240
|
+
streamState.expectedChunkIndex += 1;
|
|
3241
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
3242
|
+
streamState.stream.write(chunkBuffer);
|
|
3243
|
+
}
|
|
3244
|
+
handleIncomingClientStreamEndFrame(framePayload) {
|
|
3245
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3246
|
+
if (!streamState) {
|
|
3247
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3248
|
+
}
|
|
3249
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
3255
|
+
throw new Error(
|
|
3256
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
3260
|
+
throw new Error(
|
|
3261
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
streamState.stream.end();
|
|
3265
|
+
this.incomingStreams.delete(framePayload.streamId);
|
|
3266
|
+
}
|
|
3267
|
+
handleIncomingClientStreamAbortFrame(framePayload) {
|
|
3268
|
+
this.abortIncomingClientStream(framePayload.streamId, framePayload.reason);
|
|
3269
|
+
}
|
|
3270
|
+
handleIncomingStreamFrame(data) {
|
|
3271
|
+
let framePayload = null;
|
|
3272
|
+
try {
|
|
3273
|
+
framePayload = parseStreamFramePayload(data);
|
|
3274
|
+
if (framePayload.type === "start") {
|
|
3275
|
+
this.handleIncomingClientStreamStartFrame(framePayload);
|
|
3276
|
+
return;
|
|
3277
|
+
}
|
|
3278
|
+
if (framePayload.type === "chunk") {
|
|
3279
|
+
this.handleIncomingClientStreamChunkFrame(framePayload);
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
if (framePayload.type === "end") {
|
|
3283
|
+
this.handleIncomingClientStreamEndFrame(framePayload);
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
this.handleIncomingClientStreamAbortFrame(framePayload);
|
|
3287
|
+
} catch (error) {
|
|
3288
|
+
const normalizedError = normalizeToError(
|
|
3289
|
+
error,
|
|
3290
|
+
"Failed to process incoming stream frame on client."
|
|
3291
|
+
);
|
|
3292
|
+
if (framePayload) {
|
|
3293
|
+
this.abortIncomingClientStream(framePayload.streamId, normalizedError.message);
|
|
3294
|
+
}
|
|
3295
|
+
this.notifyError(normalizedError);
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
2432
3298
|
notifyConnect() {
|
|
2433
3299
|
for (const handler of this.connectHandlers) {
|
|
2434
3300
|
try {
|