@aegis-fluxion/core 0.7.2 → 0.9.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 +127 -1
- package/dist/index.cjs +1312 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -1
- package/dist/index.d.ts +82 -1
- package/dist/index.js +1313 -21
- 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 }; }
|
|
@@ -13,20 +14,31 @@ var DEFAULT_CLOSE_REASON = "";
|
|
|
13
14
|
var POLICY_VIOLATION_CLOSE_CODE = 1008;
|
|
14
15
|
var POLICY_VIOLATION_CLOSE_REASON = "Connection rejected by middleware.";
|
|
15
16
|
var INTERNAL_HANDSHAKE_EVENT = "__handshake";
|
|
17
|
+
var INTERNAL_SESSION_TICKET_EVENT = "__session:ticket";
|
|
16
18
|
var INTERNAL_RPC_REQUEST_EVENT = "__rpc:req";
|
|
17
19
|
var INTERNAL_RPC_RESPONSE_EVENT = "__rpc:res";
|
|
20
|
+
var INTERNAL_STREAM_FRAME_EVENT = "__stream:frame";
|
|
18
21
|
var READY_EVENT = "ready";
|
|
19
22
|
var HANDSHAKE_CURVE = "prime256v1";
|
|
23
|
+
var HANDSHAKE_PROTOCOL_VERSION = 1;
|
|
20
24
|
var ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
21
25
|
var GCM_IV_LENGTH = 12;
|
|
22
26
|
var GCM_AUTH_TAG_LENGTH = 16;
|
|
23
27
|
var ENCRYPTION_KEY_LENGTH = 32;
|
|
24
28
|
var ENCRYPTED_PACKET_VERSION = 1;
|
|
25
29
|
var ENCRYPTED_PACKET_PREFIX_LENGTH = 1 + GCM_IV_LENGTH + GCM_AUTH_TAG_LENGTH;
|
|
30
|
+
var SESSION_TICKET_VERSION = 1;
|
|
26
31
|
var BINARY_PAYLOAD_MARKER = "__afxBinaryPayload";
|
|
27
32
|
var BINARY_PAYLOAD_VERSION = 1;
|
|
28
33
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
29
34
|
var DEFAULT_HEARTBEAT_TIMEOUT_MS = 15e3;
|
|
35
|
+
var DEFAULT_SESSION_RESUMPTION_ENABLED = true;
|
|
36
|
+
var DEFAULT_SESSION_TICKET_TTL_MS = 10 * 6e4;
|
|
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;
|
|
41
|
+
var RESUMPTION_NONCE_LENGTH = 16;
|
|
30
42
|
var DEFAULT_RECONNECT_INITIAL_DELAY_MS = 250;
|
|
31
43
|
var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
32
44
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
@@ -248,7 +260,7 @@ function decodeCloseReason(reason) {
|
|
|
248
260
|
return reason.toString("utf8");
|
|
249
261
|
}
|
|
250
262
|
function isReservedEmitEvent(event) {
|
|
251
|
-
return event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_RPC_REQUEST_EVENT || event === INTERNAL_RPC_RESPONSE_EVENT || event === READY_EVENT;
|
|
263
|
+
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;
|
|
252
264
|
}
|
|
253
265
|
function isPromiseLike(value) {
|
|
254
266
|
return typeof value === "object" && value !== null && "then" in value;
|
|
@@ -260,6 +272,240 @@ function normalizeRpcTimeout(timeoutMs) {
|
|
|
260
272
|
}
|
|
261
273
|
return resolvedTimeoutMs;
|
|
262
274
|
}
|
|
275
|
+
function normalizeStreamChunkSize(chunkSizeBytes) {
|
|
276
|
+
const resolvedChunkSize = chunkSizeBytes ?? DEFAULT_STREAM_CHUNK_SIZE_BYTES;
|
|
277
|
+
if (!Number.isInteger(resolvedChunkSize) || resolvedChunkSize <= 0) {
|
|
278
|
+
throw new Error("Stream chunkSizeBytes must be a positive integer.");
|
|
279
|
+
}
|
|
280
|
+
if (resolvedChunkSize > MAX_STREAM_CHUNK_SIZE_BYTES) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Stream chunkSizeBytes cannot exceed ${MAX_STREAM_CHUNK_SIZE_BYTES} bytes.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return resolvedChunkSize;
|
|
286
|
+
}
|
|
287
|
+
function resolveKnownStreamSourceSize(source, hint) {
|
|
288
|
+
if (hint !== void 0) {
|
|
289
|
+
if (!Number.isInteger(hint) || hint < 0) {
|
|
290
|
+
throw new Error("Stream totalBytes must be a non-negative integer.");
|
|
291
|
+
}
|
|
292
|
+
return hint;
|
|
293
|
+
}
|
|
294
|
+
if (Buffer.isBuffer(source)) {
|
|
295
|
+
return source.length;
|
|
296
|
+
}
|
|
297
|
+
if (source instanceof Uint8Array) {
|
|
298
|
+
return source.byteLength;
|
|
299
|
+
}
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
function normalizeChunkSourceValue(value) {
|
|
303
|
+
if (Buffer.isBuffer(value)) {
|
|
304
|
+
return value;
|
|
305
|
+
}
|
|
306
|
+
if (value instanceof Uint8Array) {
|
|
307
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
308
|
+
}
|
|
309
|
+
if (value instanceof ArrayBuffer) {
|
|
310
|
+
return Buffer.from(value);
|
|
311
|
+
}
|
|
312
|
+
if (typeof value === "string") {
|
|
313
|
+
return Buffer.from(value, "utf8");
|
|
314
|
+
}
|
|
315
|
+
throw new Error("Stream source yielded an unsupported chunk value.");
|
|
316
|
+
}
|
|
317
|
+
function isAsyncIterableValue(value) {
|
|
318
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
319
|
+
}
|
|
320
|
+
function isReadableSource(value) {
|
|
321
|
+
return value instanceof stream.Readable;
|
|
322
|
+
}
|
|
323
|
+
function splitChunkBuffer(chunk, chunkSizeBytes) {
|
|
324
|
+
if (chunk.length <= chunkSizeBytes) {
|
|
325
|
+
return [chunk];
|
|
326
|
+
}
|
|
327
|
+
const splitChunks = [];
|
|
328
|
+
for (let offset = 0; offset < chunk.length; offset += chunkSizeBytes) {
|
|
329
|
+
splitChunks.push(chunk.subarray(offset, offset + chunkSizeBytes));
|
|
330
|
+
}
|
|
331
|
+
return splitChunks;
|
|
332
|
+
}
|
|
333
|
+
async function* createChunkStreamIterator(source, chunkSizeBytes) {
|
|
334
|
+
if (Buffer.isBuffer(source)) {
|
|
335
|
+
yield* splitChunkBuffer(source, chunkSizeBytes);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (source instanceof Uint8Array) {
|
|
339
|
+
yield* splitChunkBuffer(
|
|
340
|
+
Buffer.from(source.buffer, source.byteOffset, source.byteLength),
|
|
341
|
+
chunkSizeBytes
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (isReadableSource(source) || isAsyncIterableValue(source)) {
|
|
346
|
+
for await (const chunkValue of source) {
|
|
347
|
+
const normalizedChunk = normalizeChunkSourceValue(chunkValue);
|
|
348
|
+
if (normalizedChunk.length === 0) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
yield* splitChunkBuffer(normalizedChunk, chunkSizeBytes);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
throw new Error("Unsupported stream source type.");
|
|
356
|
+
}
|
|
357
|
+
function parseStreamFramePayload(data) {
|
|
358
|
+
if (typeof data !== "object" || data === null) {
|
|
359
|
+
throw new Error("Invalid stream frame payload format.");
|
|
360
|
+
}
|
|
361
|
+
const payload = data;
|
|
362
|
+
if (payload.version !== STREAM_FRAME_VERSION) {
|
|
363
|
+
throw new Error(`Unsupported stream frame version: ${String(payload.version)}.`);
|
|
364
|
+
}
|
|
365
|
+
if (typeof payload.streamId !== "string" || payload.streamId.trim().length === 0) {
|
|
366
|
+
throw new Error("Stream frame streamId must be a non-empty string.");
|
|
367
|
+
}
|
|
368
|
+
if (payload.type === "start") {
|
|
369
|
+
if (typeof payload.event !== "string" || payload.event.trim().length === 0) {
|
|
370
|
+
throw new Error("Stream start frame event must be a non-empty string.");
|
|
371
|
+
}
|
|
372
|
+
if (payload.totalBytes !== void 0 && (!Number.isInteger(payload.totalBytes) || payload.totalBytes < 0)) {
|
|
373
|
+
throw new Error("Stream start frame totalBytes must be a non-negative integer.");
|
|
374
|
+
}
|
|
375
|
+
if (payload.metadata !== void 0 && !isPlainObject(payload.metadata)) {
|
|
376
|
+
throw new Error("Stream start frame metadata must be a plain object when provided.");
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
version: STREAM_FRAME_VERSION,
|
|
380
|
+
type: "start",
|
|
381
|
+
streamId: payload.streamId.trim(),
|
|
382
|
+
event: payload.event.trim(),
|
|
383
|
+
...payload.metadata ? { metadata: payload.metadata } : {},
|
|
384
|
+
...payload.totalBytes !== void 0 ? { totalBytes: payload.totalBytes } : {}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (payload.type === "chunk") {
|
|
388
|
+
const { index, byteLength } = payload;
|
|
389
|
+
if (typeof index !== "number" || !Number.isInteger(index) || index < 0) {
|
|
390
|
+
throw new Error("Stream chunk frame index must be a non-negative integer.");
|
|
391
|
+
}
|
|
392
|
+
if (typeof payload.payload !== "string" || payload.payload.length === 0) {
|
|
393
|
+
throw new Error("Stream chunk frame payload must be a non-empty base64 string.");
|
|
394
|
+
}
|
|
395
|
+
if (typeof byteLength !== "number" || !Number.isInteger(byteLength) || byteLength <= 0) {
|
|
396
|
+
throw new Error("Stream chunk frame byteLength must be a positive integer.");
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
version: STREAM_FRAME_VERSION,
|
|
400
|
+
type: "chunk",
|
|
401
|
+
streamId: payload.streamId.trim(),
|
|
402
|
+
index,
|
|
403
|
+
payload: payload.payload,
|
|
404
|
+
byteLength
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
if (payload.type === "end") {
|
|
408
|
+
const { chunkCount, totalBytes } = payload;
|
|
409
|
+
if (typeof chunkCount !== "number" || !Number.isInteger(chunkCount) || chunkCount < 0) {
|
|
410
|
+
throw new Error("Stream end frame chunkCount must be a non-negative integer.");
|
|
411
|
+
}
|
|
412
|
+
if (typeof totalBytes !== "number" || !Number.isInteger(totalBytes) || totalBytes < 0) {
|
|
413
|
+
throw new Error("Stream end frame totalBytes must be a non-negative integer.");
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
version: STREAM_FRAME_VERSION,
|
|
417
|
+
type: "end",
|
|
418
|
+
streamId: payload.streamId.trim(),
|
|
419
|
+
chunkCount,
|
|
420
|
+
totalBytes
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (payload.type === "abort") {
|
|
424
|
+
if (typeof payload.reason !== "string" || payload.reason.trim().length === 0) {
|
|
425
|
+
throw new Error("Stream abort frame reason must be a non-empty string.");
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
version: STREAM_FRAME_VERSION,
|
|
429
|
+
type: "abort",
|
|
430
|
+
streamId: payload.streamId.trim(),
|
|
431
|
+
reason: payload.reason.trim()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
throw new Error("Unsupported stream frame type.");
|
|
435
|
+
}
|
|
436
|
+
async function transmitChunkedStreamFrames(event, source, options, sendFrame) {
|
|
437
|
+
const chunkSizeBytes = normalizeStreamChunkSize(options?.chunkSizeBytes);
|
|
438
|
+
const totalBytesHint = resolveKnownStreamSourceSize(source, options?.totalBytes);
|
|
439
|
+
if (options?.metadata !== void 0 && !isPlainObject(options.metadata)) {
|
|
440
|
+
throw new Error("Stream metadata must be a plain object when provided.");
|
|
441
|
+
}
|
|
442
|
+
if (options?.signal?.aborted) {
|
|
443
|
+
throw new Error("Stream transfer aborted before dispatch.");
|
|
444
|
+
}
|
|
445
|
+
const streamId = crypto.randomUUID();
|
|
446
|
+
let chunkCount = 0;
|
|
447
|
+
let totalBytes = 0;
|
|
448
|
+
await sendFrame({
|
|
449
|
+
version: STREAM_FRAME_VERSION,
|
|
450
|
+
type: "start",
|
|
451
|
+
streamId,
|
|
452
|
+
event,
|
|
453
|
+
...options?.metadata ? { metadata: options.metadata } : {},
|
|
454
|
+
...totalBytesHint !== void 0 ? { totalBytes: totalBytesHint } : {}
|
|
455
|
+
});
|
|
456
|
+
try {
|
|
457
|
+
for await (const chunkBuffer of createChunkStreamIterator(source, chunkSizeBytes)) {
|
|
458
|
+
if (options?.signal?.aborted) {
|
|
459
|
+
throw new Error("Stream transfer aborted by caller signal.");
|
|
460
|
+
}
|
|
461
|
+
if (chunkBuffer.length === 0) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
await sendFrame({
|
|
465
|
+
version: STREAM_FRAME_VERSION,
|
|
466
|
+
type: "chunk",
|
|
467
|
+
streamId,
|
|
468
|
+
index: chunkCount,
|
|
469
|
+
payload: chunkBuffer.toString("base64"),
|
|
470
|
+
byteLength: chunkBuffer.length
|
|
471
|
+
});
|
|
472
|
+
chunkCount += 1;
|
|
473
|
+
totalBytes += chunkBuffer.length;
|
|
474
|
+
}
|
|
475
|
+
if (totalBytesHint !== void 0 && totalBytes !== totalBytesHint) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
`Stream totalBytes mismatch. Expected ${totalBytesHint}, received ${totalBytes}.`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
await sendFrame({
|
|
481
|
+
version: STREAM_FRAME_VERSION,
|
|
482
|
+
type: "end",
|
|
483
|
+
streamId,
|
|
484
|
+
chunkCount,
|
|
485
|
+
totalBytes
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
streamId,
|
|
489
|
+
chunkCount,
|
|
490
|
+
totalBytes
|
|
491
|
+
};
|
|
492
|
+
} catch (error) {
|
|
493
|
+
const normalizedError = normalizeToError(
|
|
494
|
+
error,
|
|
495
|
+
`Chunked stream transfer failed for event "${event}".`
|
|
496
|
+
);
|
|
497
|
+
try {
|
|
498
|
+
await sendFrame({
|
|
499
|
+
version: STREAM_FRAME_VERSION,
|
|
500
|
+
type: "abort",
|
|
501
|
+
streamId,
|
|
502
|
+
reason: normalizedError.message
|
|
503
|
+
});
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
throw normalizedError;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
263
509
|
function parseRpcRequestPayload(data) {
|
|
264
510
|
if (typeof data !== "object" || data === null) {
|
|
265
511
|
throw new Error("Invalid RPC request payload format.");
|
|
@@ -339,16 +585,149 @@ function createEphemeralHandshakeState() {
|
|
|
339
585
|
localPublicKey: ecdh.getPublicKey("base64")
|
|
340
586
|
};
|
|
341
587
|
}
|
|
588
|
+
function decodeBase64ToBuffer(value, fieldName) {
|
|
589
|
+
if (typeof value !== "string") {
|
|
590
|
+
throw new Error(`${fieldName} must be a base64 string.`);
|
|
591
|
+
}
|
|
592
|
+
const normalizedValue = value.trim();
|
|
593
|
+
if (normalizedValue.length === 0) {
|
|
594
|
+
throw new Error(`${fieldName} must be a non-empty base64 string.`);
|
|
595
|
+
}
|
|
596
|
+
const decodedBuffer = Buffer.from(normalizedValue, "base64");
|
|
597
|
+
if (decodedBuffer.length === 0) {
|
|
598
|
+
throw new Error(`${fieldName} could not be decoded from base64.`);
|
|
599
|
+
}
|
|
600
|
+
const canonicalInput = normalizedValue.replace(/=+$/u, "");
|
|
601
|
+
const canonicalDecoded = decodedBuffer.toString("base64").replace(/=+$/u, "");
|
|
602
|
+
if (canonicalInput !== canonicalDecoded) {
|
|
603
|
+
throw new Error(`${fieldName} is not valid base64 content.`);
|
|
604
|
+
}
|
|
605
|
+
return decodedBuffer;
|
|
606
|
+
}
|
|
607
|
+
function equalsConstantTime(left, right) {
|
|
608
|
+
if (left.length !== right.length) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
return crypto.timingSafeEqual(left, right);
|
|
612
|
+
}
|
|
613
|
+
function createResumeClientProof(sessionSecret, sessionId, clientNonce) {
|
|
614
|
+
return crypto.createHmac("sha256", sessionSecret).update("afx-resume-client-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
615
|
+
}
|
|
616
|
+
function createResumeServerProof(resumedKey, sessionId, clientNonce) {
|
|
617
|
+
return crypto.createHmac("sha256", resumedKey).update("afx-resume-server-proof:v1").update(sessionId).update(clientNonce).digest();
|
|
618
|
+
}
|
|
619
|
+
function deriveSessionTicketSecret(baseKey) {
|
|
620
|
+
return crypto.createHmac("sha256", baseKey).update("afx-session-ticket:v1").digest();
|
|
621
|
+
}
|
|
622
|
+
function deriveResumedEncryptionKey(sessionSecret, clientNonce) {
|
|
623
|
+
const derivedKey = crypto.createHash("sha256").update("afx-resume-encryption-key:v1").update(sessionSecret).update(clientNonce).digest();
|
|
624
|
+
if (derivedKey.length !== ENCRYPTION_KEY_LENGTH) {
|
|
625
|
+
throw new Error("Failed to derive a valid resumed AES-256 key.");
|
|
626
|
+
}
|
|
627
|
+
return derivedKey;
|
|
628
|
+
}
|
|
342
629
|
function parseHandshakePayload(data) {
|
|
343
630
|
if (typeof data !== "object" || data === null) {
|
|
344
631
|
throw new Error("Invalid handshake payload format.");
|
|
345
632
|
}
|
|
346
633
|
const payload = data;
|
|
347
|
-
if (typeof payload.
|
|
348
|
-
|
|
634
|
+
if (typeof payload.type !== "string") {
|
|
635
|
+
if (typeof payload.publicKey === "string" && payload.publicKey.length > 0) {
|
|
636
|
+
return {
|
|
637
|
+
type: "hello",
|
|
638
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
639
|
+
publicKey: payload.publicKey
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
throw new Error("Handshake payload must include a valid type.");
|
|
643
|
+
}
|
|
644
|
+
const protocolVersion = payload.protocolVersion === void 0 ? HANDSHAKE_PROTOCOL_VERSION : payload.protocolVersion;
|
|
645
|
+
if (protocolVersion !== HANDSHAKE_PROTOCOL_VERSION) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Unsupported handshake protocol version: ${String(protocolVersion)}.`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (payload.type === "hello") {
|
|
651
|
+
if (typeof payload.publicKey !== "string" || payload.publicKey.length === 0) {
|
|
652
|
+
throw new Error("Handshake hello payload must include a non-empty public key.");
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
type: "hello",
|
|
656
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
657
|
+
publicKey: payload.publicKey
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (payload.type === "resume") {
|
|
661
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
662
|
+
throw new Error("Handshake resume payload must include a non-empty sessionId.");
|
|
663
|
+
}
|
|
664
|
+
if (typeof payload.clientNonce !== "string" || payload.clientNonce.length === 0) {
|
|
665
|
+
throw new Error("Handshake resume payload must include a non-empty clientNonce.");
|
|
666
|
+
}
|
|
667
|
+
if (typeof payload.clientProof !== "string" || payload.clientProof.length === 0) {
|
|
668
|
+
throw new Error("Handshake resume payload must include a non-empty clientProof.");
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
type: "resume",
|
|
672
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
673
|
+
sessionId: payload.sessionId.trim(),
|
|
674
|
+
clientNonce: payload.clientNonce,
|
|
675
|
+
clientProof: payload.clientProof
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (payload.type === "resume-ack") {
|
|
679
|
+
if (typeof payload.ok !== "boolean") {
|
|
680
|
+
throw new Error("Handshake resume-ack payload must include boolean ok.");
|
|
681
|
+
}
|
|
682
|
+
const normalizedPayload = {
|
|
683
|
+
type: "resume-ack",
|
|
684
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
685
|
+
ok: payload.ok
|
|
686
|
+
};
|
|
687
|
+
if (typeof payload.sessionId === "string" && payload.sessionId.trim().length > 0) {
|
|
688
|
+
normalizedPayload.sessionId = payload.sessionId.trim();
|
|
689
|
+
}
|
|
690
|
+
if (typeof payload.serverProof === "string" && payload.serverProof.length > 0) {
|
|
691
|
+
normalizedPayload.serverProof = payload.serverProof;
|
|
692
|
+
}
|
|
693
|
+
if (typeof payload.reason === "string" && payload.reason.trim().length > 0) {
|
|
694
|
+
normalizedPayload.reason = payload.reason.trim();
|
|
695
|
+
}
|
|
696
|
+
return normalizedPayload;
|
|
697
|
+
}
|
|
698
|
+
throw new Error(`Unsupported handshake payload type: ${payload.type}.`);
|
|
699
|
+
}
|
|
700
|
+
function parseSessionTicketPayload(data) {
|
|
701
|
+
if (typeof data !== "object" || data === null) {
|
|
702
|
+
throw new Error("Invalid session ticket payload format.");
|
|
703
|
+
}
|
|
704
|
+
const payload = data;
|
|
705
|
+
if (payload.version !== SESSION_TICKET_VERSION) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`Unsupported session ticket payload version: ${String(payload.version)}.`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (typeof payload.sessionId !== "string" || payload.sessionId.trim().length === 0) {
|
|
711
|
+
throw new Error("Session ticket payload must include a non-empty sessionId.");
|
|
712
|
+
}
|
|
713
|
+
if (typeof payload.secret !== "string" || payload.secret.length === 0) {
|
|
714
|
+
throw new Error("Session ticket payload must include a non-empty secret.");
|
|
715
|
+
}
|
|
716
|
+
if (typeof payload.issuedAt !== "number" || !Number.isFinite(payload.issuedAt)) {
|
|
717
|
+
throw new Error("Session ticket payload issuedAt must be a finite number.");
|
|
718
|
+
}
|
|
719
|
+
if (typeof payload.expiresAt !== "number" || !Number.isFinite(payload.expiresAt)) {
|
|
720
|
+
throw new Error("Session ticket payload expiresAt must be a finite number.");
|
|
721
|
+
}
|
|
722
|
+
if (payload.expiresAt <= payload.issuedAt) {
|
|
723
|
+
throw new Error("Session ticket payload expiresAt must be greater than issuedAt.");
|
|
349
724
|
}
|
|
350
725
|
return {
|
|
351
|
-
|
|
726
|
+
version: SESSION_TICKET_VERSION,
|
|
727
|
+
sessionId: payload.sessionId.trim(),
|
|
728
|
+
secret: payload.secret,
|
|
729
|
+
issuedAt: payload.issuedAt,
|
|
730
|
+
expiresAt: payload.expiresAt
|
|
352
731
|
};
|
|
353
732
|
}
|
|
354
733
|
function deriveEncryptionKey(sharedSecret) {
|
|
@@ -418,10 +797,12 @@ var SecureServer = class {
|
|
|
418
797
|
adapter = null;
|
|
419
798
|
heartbeatConfig;
|
|
420
799
|
rateLimitConfig;
|
|
800
|
+
sessionResumptionConfig;
|
|
421
801
|
heartbeatIntervalHandle = null;
|
|
422
802
|
clientsById = /* @__PURE__ */ new Map();
|
|
423
803
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
424
804
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
805
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
425
806
|
connectionHandlers = /* @__PURE__ */ new Set();
|
|
426
807
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
427
808
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -432,6 +813,7 @@ var SecureServer = class {
|
|
|
432
813
|
sharedSecretBySocket = /* @__PURE__ */ new WeakMap();
|
|
433
814
|
encryptionKeyBySocket = /* @__PURE__ */ new WeakMap();
|
|
434
815
|
pendingPayloadsBySocket = /* @__PURE__ */ new WeakMap();
|
|
816
|
+
incomingStreamsBySocket = /* @__PURE__ */ new WeakMap();
|
|
435
817
|
pendingRpcRequestsBySocket = /* @__PURE__ */ new WeakMap();
|
|
436
818
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
437
819
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
@@ -439,10 +821,12 @@ var SecureServer = class {
|
|
|
439
821
|
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
440
822
|
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
441
823
|
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
824
|
+
sessionTicketStore = /* @__PURE__ */ new Map();
|
|
442
825
|
constructor(options) {
|
|
443
|
-
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
826
|
+
const { heartbeat, rateLimit, sessionResumption, adapter, ...socketServerOptions } = options;
|
|
444
827
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
445
828
|
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
829
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(sessionResumption);
|
|
446
830
|
this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
|
|
447
831
|
this.bindSocketServerEvents();
|
|
448
832
|
this.startHeartbeatLoop();
|
|
@@ -528,8 +912,8 @@ var SecureServer = class {
|
|
|
528
912
|
this.errorHandlers.add(handler);
|
|
529
913
|
return this;
|
|
530
914
|
}
|
|
531
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
532
|
-
throw new Error(`The event "${
|
|
915
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
916
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
533
917
|
}
|
|
534
918
|
const typedHandler = handler;
|
|
535
919
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -560,7 +944,7 @@ var SecureServer = class {
|
|
|
560
944
|
this.errorHandlers.delete(handler);
|
|
561
945
|
return this;
|
|
562
946
|
}
|
|
563
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
947
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
564
948
|
return this;
|
|
565
949
|
}
|
|
566
950
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -578,6 +962,38 @@ var SecureServer = class {
|
|
|
578
962
|
}
|
|
579
963
|
return this;
|
|
580
964
|
}
|
|
965
|
+
onStream(event, handler) {
|
|
966
|
+
try {
|
|
967
|
+
if (isReservedEmitEvent(event)) {
|
|
968
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
969
|
+
}
|
|
970
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
971
|
+
listeners.add(handler);
|
|
972
|
+
this.streamEventHandlers.set(event, listeners);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
this.notifyError(
|
|
975
|
+
normalizeToError(error, "Failed to register server stream handler.")
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
return this;
|
|
979
|
+
}
|
|
980
|
+
offStream(event, handler) {
|
|
981
|
+
try {
|
|
982
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
983
|
+
if (!listeners) {
|
|
984
|
+
return this;
|
|
985
|
+
}
|
|
986
|
+
listeners.delete(handler);
|
|
987
|
+
if (listeners.size === 0) {
|
|
988
|
+
this.streamEventHandlers.delete(event);
|
|
989
|
+
}
|
|
990
|
+
} catch (error) {
|
|
991
|
+
this.notifyError(
|
|
992
|
+
normalizeToError(error, "Failed to remove server stream handler.")
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
return this;
|
|
996
|
+
}
|
|
581
997
|
use(middleware) {
|
|
582
998
|
try {
|
|
583
999
|
if (typeof middleware !== "function") {
|
|
@@ -653,6 +1069,40 @@ var SecureServer = class {
|
|
|
653
1069
|
return false;
|
|
654
1070
|
}
|
|
655
1071
|
}
|
|
1072
|
+
async emitStreamTo(clientId, event, source, options) {
|
|
1073
|
+
try {
|
|
1074
|
+
if (isReservedEmitEvent(event)) {
|
|
1075
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1076
|
+
}
|
|
1077
|
+
const client = this.clientsById.get(clientId);
|
|
1078
|
+
if (!client) {
|
|
1079
|
+
throw new Error(`Client with id ${clientId} was not found.`);
|
|
1080
|
+
}
|
|
1081
|
+
if (!this.isClientHandshakeReady(client.socket)) {
|
|
1082
|
+
throw new Error(
|
|
1083
|
+
`Cannot stream event "${event}" before secure handshake completion for client ${client.id}.`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
return await transmitChunkedStreamFrames(
|
|
1087
|
+
event,
|
|
1088
|
+
source,
|
|
1089
|
+
options,
|
|
1090
|
+
async (framePayload) => {
|
|
1091
|
+
await this.sendEncryptedEnvelope(client.socket, {
|
|
1092
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
1093
|
+
data: framePayload
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
const normalizedError = normalizeToError(
|
|
1099
|
+
error,
|
|
1100
|
+
`Failed to emit chunked stream event "${event}" to client ${clientId}.`
|
|
1101
|
+
);
|
|
1102
|
+
this.notifyError(normalizedError);
|
|
1103
|
+
throw normalizedError;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
656
1106
|
to(room) {
|
|
657
1107
|
const normalizedRoom = this.normalizeRoomName(room);
|
|
658
1108
|
return {
|
|
@@ -685,6 +1135,10 @@ var SecureServer = class {
|
|
|
685
1135
|
client.socket,
|
|
686
1136
|
new Error("Server closed before ACK response was received.")
|
|
687
1137
|
);
|
|
1138
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1139
|
+
client.socket,
|
|
1140
|
+
"Server closed before stream transfer completed."
|
|
1141
|
+
);
|
|
688
1142
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
689
1143
|
if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
|
|
690
1144
|
client.socket.close(code, reason);
|
|
@@ -693,6 +1147,7 @@ var SecureServer = class {
|
|
|
693
1147
|
this.rateLimitBucketsByClientId.clear();
|
|
694
1148
|
this.rateLimitBucketsByIp.clear();
|
|
695
1149
|
this.clientIpByClientId.clear();
|
|
1150
|
+
this.sessionTicketStore.clear();
|
|
696
1151
|
this.socketServer.close();
|
|
697
1152
|
} catch (error) {
|
|
698
1153
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -769,6 +1224,94 @@ var SecureServer = class {
|
|
|
769
1224
|
disconnectReason
|
|
770
1225
|
};
|
|
771
1226
|
}
|
|
1227
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
1228
|
+
const ticketTtlMs = sessionResumptionOptions?.ticketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
1229
|
+
const maxCachedTickets = sessionResumptionOptions?.maxCachedTickets ?? DEFAULT_SESSION_TICKET_MAX_CACHE_SIZE;
|
|
1230
|
+
if (!Number.isFinite(ticketTtlMs) || ticketTtlMs <= 0) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
"Server sessionResumption ticketTtlMs must be a positive number."
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
if (!Number.isInteger(maxCachedTickets) || maxCachedTickets <= 0) {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
"Server sessionResumption maxCachedTickets must be a positive integer."
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
1242
|
+
ticketTtlMs,
|
|
1243
|
+
maxCachedTickets
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
pruneExpiredSessionTickets(now) {
|
|
1247
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
1248
|
+
if (ticketRecord.expiresAt <= now) {
|
|
1249
|
+
this.sessionTicketStore.delete(sessionId);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
evictSessionTicketsIfNeeded() {
|
|
1254
|
+
while (this.sessionTicketStore.size > this.sessionResumptionConfig.maxCachedTickets) {
|
|
1255
|
+
let oldestSessionId = null;
|
|
1256
|
+
let oldestIssuedAt = Number.POSITIVE_INFINITY;
|
|
1257
|
+
for (const [sessionId, ticketRecord] of this.sessionTicketStore.entries()) {
|
|
1258
|
+
if (ticketRecord.issuedAt < oldestIssuedAt) {
|
|
1259
|
+
oldestIssuedAt = ticketRecord.issuedAt;
|
|
1260
|
+
oldestSessionId = sessionId;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
if (!oldestSessionId) {
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
this.sessionTicketStore.delete(oldestSessionId);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
getSessionTicket(sessionId) {
|
|
1270
|
+
const now = Date.now();
|
|
1271
|
+
this.pruneExpiredSessionTickets(now);
|
|
1272
|
+
const ticketRecord = this.sessionTicketStore.get(sessionId);
|
|
1273
|
+
if (!ticketRecord) {
|
|
1274
|
+
return null;
|
|
1275
|
+
}
|
|
1276
|
+
if (ticketRecord.expiresAt <= now) {
|
|
1277
|
+
this.sessionTicketStore.delete(sessionId);
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
return ticketRecord;
|
|
1281
|
+
}
|
|
1282
|
+
issueSessionTicket(socket, baseKey) {
|
|
1283
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const now = Date.now();
|
|
1287
|
+
this.pruneExpiredSessionTickets(now);
|
|
1288
|
+
const sessionId = crypto.randomUUID();
|
|
1289
|
+
const sessionSecret = deriveSessionTicketSecret(baseKey);
|
|
1290
|
+
const expiresAt = now + this.sessionResumptionConfig.ticketTtlMs;
|
|
1291
|
+
const ticketRecord = {
|
|
1292
|
+
sessionId,
|
|
1293
|
+
secret: sessionSecret,
|
|
1294
|
+
issuedAt: now,
|
|
1295
|
+
expiresAt
|
|
1296
|
+
};
|
|
1297
|
+
this.sessionTicketStore.set(sessionId, ticketRecord);
|
|
1298
|
+
this.evictSessionTicketsIfNeeded();
|
|
1299
|
+
const ticketPayload = {
|
|
1300
|
+
version: SESSION_TICKET_VERSION,
|
|
1301
|
+
sessionId,
|
|
1302
|
+
secret: sessionSecret.toString("base64"),
|
|
1303
|
+
issuedAt: now,
|
|
1304
|
+
expiresAt
|
|
1305
|
+
};
|
|
1306
|
+
void this.sendOrQueuePayload(socket, {
|
|
1307
|
+
event: INTERNAL_SESSION_TICKET_EVENT,
|
|
1308
|
+
data: ticketPayload
|
|
1309
|
+
}).catch((error) => {
|
|
1310
|
+
this.notifyError(
|
|
1311
|
+
normalizeToError(error, "Failed to deliver secure session ticket.")
|
|
1312
|
+
);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
772
1315
|
createRateLimitBucket(now) {
|
|
773
1316
|
return {
|
|
774
1317
|
windowStartedAt: now,
|
|
@@ -1139,6 +1682,18 @@ var SecureServer = class {
|
|
|
1139
1682
|
await this.handleRpcRequest(client, decryptedEnvelope.data);
|
|
1140
1683
|
return;
|
|
1141
1684
|
}
|
|
1685
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
1686
|
+
this.handleIncomingStreamFrame(client, decryptedEnvelope.data);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1690
|
+
this.notifyError(
|
|
1691
|
+
new Error(
|
|
1692
|
+
`Client ${client.id} attempted to send reserved internal session ticket event.`
|
|
1693
|
+
)
|
|
1694
|
+
);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1142
1697
|
const interceptedData = await this.applyMessageMiddleware(
|
|
1143
1698
|
"incoming",
|
|
1144
1699
|
client,
|
|
@@ -1172,6 +1727,10 @@ var SecureServer = class {
|
|
|
1172
1727
|
this.pendingRpcRequestsBySocket.delete(client.socket);
|
|
1173
1728
|
this.heartbeatStateBySocket.delete(client.socket);
|
|
1174
1729
|
this.middlewareMetadataBySocket.delete(client.socket);
|
|
1730
|
+
this.cleanupIncomingStreamsForSocket(
|
|
1731
|
+
client.socket,
|
|
1732
|
+
`Client ${client.id} disconnected before stream transfer completed.`
|
|
1733
|
+
);
|
|
1175
1734
|
const decodedReason = decodeCloseReason(reason);
|
|
1176
1735
|
for (const handler of this.disconnectHandlers) {
|
|
1177
1736
|
try {
|
|
@@ -1217,6 +1776,197 @@ var SecureServer = class {
|
|
|
1217
1776
|
}
|
|
1218
1777
|
}
|
|
1219
1778
|
}
|
|
1779
|
+
getOrCreateIncomingServerStreams(socket) {
|
|
1780
|
+
const existingStreams = this.incomingStreamsBySocket.get(socket);
|
|
1781
|
+
if (existingStreams) {
|
|
1782
|
+
return existingStreams;
|
|
1783
|
+
}
|
|
1784
|
+
const streamMap = /* @__PURE__ */ new Map();
|
|
1785
|
+
this.incomingStreamsBySocket.set(socket, streamMap);
|
|
1786
|
+
return streamMap;
|
|
1787
|
+
}
|
|
1788
|
+
cleanupIncomingStreamsForSocket(socket, reason) {
|
|
1789
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1790
|
+
if (!streamMap) {
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
for (const streamState of streamMap.values()) {
|
|
1794
|
+
streamState.stream.destroy(new Error(reason));
|
|
1795
|
+
}
|
|
1796
|
+
streamMap.clear();
|
|
1797
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1798
|
+
}
|
|
1799
|
+
abortIncomingServerStream(socket, streamId, reason) {
|
|
1800
|
+
const streamMap = this.incomingStreamsBySocket.get(socket);
|
|
1801
|
+
if (!streamMap) {
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const streamState = streamMap.get(streamId);
|
|
1805
|
+
if (!streamState) {
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
streamState.stream.destroy(new Error(reason));
|
|
1809
|
+
streamMap.delete(streamId);
|
|
1810
|
+
if (streamMap.size === 0) {
|
|
1811
|
+
this.incomingStreamsBySocket.delete(socket);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
dispatchServerStreamEvent(event, stream, info, client) {
|
|
1815
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
1816
|
+
if (!handlers || handlers.size === 0) {
|
|
1817
|
+
stream.resume();
|
|
1818
|
+
this.notifyError(
|
|
1819
|
+
new Error(
|
|
1820
|
+
`No stream handler is registered for event "${event}" on server client ${client.id}.`
|
|
1821
|
+
)
|
|
1822
|
+
);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
for (const handler of handlers) {
|
|
1826
|
+
try {
|
|
1827
|
+
const handlerResult = handler(stream, info, client);
|
|
1828
|
+
if (isPromiseLike(handlerResult)) {
|
|
1829
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
1830
|
+
this.notifyError(
|
|
1831
|
+
normalizeToError(
|
|
1832
|
+
error,
|
|
1833
|
+
`Server stream handler failed for event ${event}.`
|
|
1834
|
+
)
|
|
1835
|
+
);
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
this.notifyError(
|
|
1840
|
+
normalizeToError(
|
|
1841
|
+
error,
|
|
1842
|
+
`Server stream handler failed for event ${event}.`
|
|
1843
|
+
)
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
handleIncomingStreamStartFrame(client, framePayload) {
|
|
1849
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
const incomingStreams = this.getOrCreateIncomingServerStreams(client.socket);
|
|
1855
|
+
if (incomingStreams.has(framePayload.streamId)) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
`Stream ${framePayload.streamId} already exists for client ${client.id}.`
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const stream$1 = new stream.PassThrough();
|
|
1861
|
+
const streamInfo = {
|
|
1862
|
+
streamId: framePayload.streamId,
|
|
1863
|
+
event: framePayload.event,
|
|
1864
|
+
startedAt: Date.now(),
|
|
1865
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
1866
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
1867
|
+
};
|
|
1868
|
+
incomingStreams.set(framePayload.streamId, {
|
|
1869
|
+
info: streamInfo,
|
|
1870
|
+
stream: stream$1,
|
|
1871
|
+
expectedChunkIndex: 0,
|
|
1872
|
+
receivedBytes: 0
|
|
1873
|
+
});
|
|
1874
|
+
this.dispatchServerStreamEvent(framePayload.event, stream$1, streamInfo, client);
|
|
1875
|
+
}
|
|
1876
|
+
handleIncomingStreamChunkFrame(client, framePayload) {
|
|
1877
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1878
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1879
|
+
if (!incomingStreams || !streamState) {
|
|
1880
|
+
throw new Error(
|
|
1881
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
1890
|
+
framePayload.payload,
|
|
1891
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
1892
|
+
);
|
|
1893
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
streamState.expectedChunkIndex += 1;
|
|
1899
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
1900
|
+
streamState.stream.write(chunkBuffer);
|
|
1901
|
+
}
|
|
1902
|
+
handleIncomingStreamEndFrame(client, framePayload) {
|
|
1903
|
+
const incomingStreams = this.incomingStreamsBySocket.get(client.socket);
|
|
1904
|
+
const streamState = incomingStreams?.get(framePayload.streamId);
|
|
1905
|
+
if (!incomingStreams || !streamState) {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`Stream ${framePayload.streamId} is unknown for client ${client.id}.`
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
1916
|
+
throw new Error(
|
|
1917
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
1921
|
+
throw new Error(
|
|
1922
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
streamState.stream.end();
|
|
1926
|
+
incomingStreams.delete(framePayload.streamId);
|
|
1927
|
+
if (incomingStreams.size === 0) {
|
|
1928
|
+
this.incomingStreamsBySocket.delete(client.socket);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
handleIncomingStreamAbortFrame(client, framePayload) {
|
|
1932
|
+
this.abortIncomingServerStream(
|
|
1933
|
+
client.socket,
|
|
1934
|
+
framePayload.streamId,
|
|
1935
|
+
framePayload.reason
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
handleIncomingStreamFrame(client, data) {
|
|
1939
|
+
let framePayload = null;
|
|
1940
|
+
try {
|
|
1941
|
+
framePayload = parseStreamFramePayload(data);
|
|
1942
|
+
if (framePayload.type === "start") {
|
|
1943
|
+
this.handleIncomingStreamStartFrame(client, framePayload);
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
if (framePayload.type === "chunk") {
|
|
1947
|
+
this.handleIncomingStreamChunkFrame(client, framePayload);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
if (framePayload.type === "end") {
|
|
1951
|
+
this.handleIncomingStreamEndFrame(client, framePayload);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
this.handleIncomingStreamAbortFrame(client, framePayload);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
const normalizedError = normalizeToError(
|
|
1957
|
+
error,
|
|
1958
|
+
`Failed to process incoming stream frame for client ${client.id}.`
|
|
1959
|
+
);
|
|
1960
|
+
if (framePayload) {
|
|
1961
|
+
this.abortIncomingServerStream(
|
|
1962
|
+
client.socket,
|
|
1963
|
+
framePayload.streamId,
|
|
1964
|
+
normalizedError.message
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
this.notifyError(normalizedError);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1220
1970
|
async executeServerMiddleware(context) {
|
|
1221
1971
|
if (this.middlewareHandlers.length === 0) {
|
|
1222
1972
|
return;
|
|
@@ -1452,10 +2202,104 @@ var SecureServer = class {
|
|
|
1452
2202
|
this.sendRaw(
|
|
1453
2203
|
socket,
|
|
1454
2204
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
2205
|
+
type: "hello",
|
|
2206
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
1455
2207
|
publicKey: localPublicKey
|
|
1456
2208
|
})
|
|
1457
2209
|
);
|
|
1458
2210
|
}
|
|
2211
|
+
sendResumeAck(socket, payload) {
|
|
2212
|
+
const responsePayload = {
|
|
2213
|
+
type: "resume-ack",
|
|
2214
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2215
|
+
ok: payload.ok
|
|
2216
|
+
};
|
|
2217
|
+
if (payload.sessionId !== void 0 && payload.sessionId.length > 0) {
|
|
2218
|
+
responsePayload.sessionId = payload.sessionId;
|
|
2219
|
+
}
|
|
2220
|
+
if (payload.serverProof !== void 0 && payload.serverProof.length > 0) {
|
|
2221
|
+
responsePayload.serverProof = payload.serverProof;
|
|
2222
|
+
}
|
|
2223
|
+
if (payload.reason !== void 0 && payload.reason.length > 0) {
|
|
2224
|
+
responsePayload.reason = payload.reason;
|
|
2225
|
+
}
|
|
2226
|
+
this.sendRaw(
|
|
2227
|
+
socket,
|
|
2228
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, responsePayload)
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
handleResumeHandshake(client, payload) {
|
|
2232
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
2233
|
+
this.sendResumeAck(client.socket, {
|
|
2234
|
+
ok: false,
|
|
2235
|
+
reason: "Session resumption is disabled."
|
|
2236
|
+
});
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const ticketRecord = this.getSessionTicket(payload.sessionId);
|
|
2240
|
+
if (!ticketRecord) {
|
|
2241
|
+
this.sendResumeAck(client.socket, {
|
|
2242
|
+
ok: false,
|
|
2243
|
+
reason: "Session ticket is unknown or expired."
|
|
2244
|
+
});
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
try {
|
|
2248
|
+
const clientNonce = decodeBase64ToBuffer(
|
|
2249
|
+
payload.clientNonce,
|
|
2250
|
+
"Handshake resume clientNonce"
|
|
2251
|
+
);
|
|
2252
|
+
if (clientNonce.length !== RESUMPTION_NONCE_LENGTH) {
|
|
2253
|
+
throw new Error(
|
|
2254
|
+
`Handshake resume clientNonce must be ${RESUMPTION_NONCE_LENGTH} bytes.`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
const receivedProof = decodeBase64ToBuffer(
|
|
2258
|
+
payload.clientProof,
|
|
2259
|
+
"Handshake resume clientProof"
|
|
2260
|
+
);
|
|
2261
|
+
const expectedProof = createResumeClientProof(
|
|
2262
|
+
ticketRecord.secret,
|
|
2263
|
+
ticketRecord.sessionId,
|
|
2264
|
+
clientNonce
|
|
2265
|
+
);
|
|
2266
|
+
if (!equalsConstantTime(receivedProof, expectedProof)) {
|
|
2267
|
+
this.sendResumeAck(client.socket, {
|
|
2268
|
+
ok: false,
|
|
2269
|
+
reason: "Session resumption proof validation failed."
|
|
2270
|
+
});
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
this.sessionTicketStore.delete(ticketRecord.sessionId);
|
|
2274
|
+
const resumedKey = deriveResumedEncryptionKey(ticketRecord.secret, clientNonce);
|
|
2275
|
+
const serverProof = createResumeServerProof(
|
|
2276
|
+
resumedKey,
|
|
2277
|
+
ticketRecord.sessionId,
|
|
2278
|
+
clientNonce
|
|
2279
|
+
).toString("base64");
|
|
2280
|
+
const handshakeState = this.handshakeStateBySocket.get(client.socket);
|
|
2281
|
+
if (!handshakeState) {
|
|
2282
|
+
throw new Error(`Missing handshake state for client ${client.id}.`);
|
|
2283
|
+
}
|
|
2284
|
+
this.sharedSecretBySocket.set(client.socket, resumedKey);
|
|
2285
|
+
this.encryptionKeyBySocket.set(client.socket, resumedKey);
|
|
2286
|
+
handshakeState.isReady = true;
|
|
2287
|
+
this.sendResumeAck(client.socket, {
|
|
2288
|
+
ok: true,
|
|
2289
|
+
sessionId: ticketRecord.sessionId,
|
|
2290
|
+
serverProof
|
|
2291
|
+
});
|
|
2292
|
+
void this.flushQueuedPayloads(client.socket);
|
|
2293
|
+
this.notifyReady(client);
|
|
2294
|
+
this.issueSessionTicket(client.socket, resumedKey);
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
this.sendResumeAck(client.socket, {
|
|
2297
|
+
ok: false,
|
|
2298
|
+
reason: "Session resumption payload was invalid."
|
|
2299
|
+
});
|
|
2300
|
+
this.notifyError(normalizeToError(error, "Failed to resume secure server session."));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
1459
2303
|
handleInternalHandshake(client, data) {
|
|
1460
2304
|
try {
|
|
1461
2305
|
const payload = parseHandshakePayload(data);
|
|
@@ -1466,14 +2310,22 @@ var SecureServer = class {
|
|
|
1466
2310
|
if (handshakeState.isReady) {
|
|
1467
2311
|
return;
|
|
1468
2312
|
}
|
|
2313
|
+
if (payload.type === "resume") {
|
|
2314
|
+
this.handleResumeHandshake(client, payload);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
if (payload.type === "resume-ack") {
|
|
2318
|
+
throw new Error("SecureServer received unexpected resume-ack handshake payload.");
|
|
2319
|
+
}
|
|
1469
2320
|
const remotePublicKey = Buffer.from(payload.publicKey, "base64");
|
|
1470
2321
|
const sharedSecret = handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
1471
2322
|
const encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
1472
2323
|
this.sharedSecretBySocket.set(client.socket, sharedSecret);
|
|
1473
2324
|
this.encryptionKeyBySocket.set(client.socket, encryptionKey);
|
|
1474
2325
|
handshakeState.isReady = true;
|
|
1475
|
-
this.flushQueuedPayloads(client.socket);
|
|
2326
|
+
void this.flushQueuedPayloads(client.socket);
|
|
1476
2327
|
this.notifyReady(client);
|
|
2328
|
+
this.issueSessionTicket(client.socket, encryptionKey);
|
|
1477
2329
|
} catch (error) {
|
|
1478
2330
|
this.notifyError(normalizeToError(error, "Failed to complete server handshake."));
|
|
1479
2331
|
}
|
|
@@ -1543,6 +2395,9 @@ var SecureServer = class {
|
|
|
1543
2395
|
}
|
|
1544
2396
|
return this.emitTo(clientId, event, data, callbackOrOptions ?? {});
|
|
1545
2397
|
},
|
|
2398
|
+
emitStream: (event, source, options) => {
|
|
2399
|
+
return this.emitStreamTo(clientId, event, source, options);
|
|
2400
|
+
},
|
|
1546
2401
|
join: (room) => this.joinClientToRoom(clientId, room),
|
|
1547
2402
|
leave: (room) => this.leaveClientFromRoom(clientId, room),
|
|
1548
2403
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
@@ -1687,6 +2542,9 @@ var SecureClient = class {
|
|
|
1687
2542
|
this.url = url;
|
|
1688
2543
|
this.options = options;
|
|
1689
2544
|
this.reconnectConfig = this.resolveReconnectConfig(this.options.reconnect);
|
|
2545
|
+
this.sessionResumptionConfig = this.resolveSessionResumptionConfig(
|
|
2546
|
+
this.options.sessionResumption
|
|
2547
|
+
);
|
|
1690
2548
|
if (this.options.autoConnect ?? true) {
|
|
1691
2549
|
this.connect();
|
|
1692
2550
|
}
|
|
@@ -1695,10 +2553,12 @@ var SecureClient = class {
|
|
|
1695
2553
|
options;
|
|
1696
2554
|
socket = null;
|
|
1697
2555
|
reconnectConfig;
|
|
2556
|
+
sessionResumptionConfig;
|
|
1698
2557
|
reconnectAttemptCount = 0;
|
|
1699
2558
|
reconnectTimer = null;
|
|
1700
2559
|
isManualDisconnectRequested = false;
|
|
1701
2560
|
customEventHandlers = /* @__PURE__ */ new Map();
|
|
2561
|
+
streamEventHandlers = /* @__PURE__ */ new Map();
|
|
1702
2562
|
connectHandlers = /* @__PURE__ */ new Set();
|
|
1703
2563
|
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
1704
2564
|
readyHandlers = /* @__PURE__ */ new Set();
|
|
@@ -1706,6 +2566,8 @@ var SecureClient = class {
|
|
|
1706
2566
|
handshakeState = null;
|
|
1707
2567
|
pendingPayloadQueue = [];
|
|
1708
2568
|
pendingRpcRequests = /* @__PURE__ */ new Map();
|
|
2569
|
+
incomingStreams = /* @__PURE__ */ new Map();
|
|
2570
|
+
sessionTicket = null;
|
|
1709
2571
|
get readyState() {
|
|
1710
2572
|
return this.socket?.readyState ?? null;
|
|
1711
2573
|
}
|
|
@@ -1764,8 +2626,8 @@ var SecureClient = class {
|
|
|
1764
2626
|
this.errorHandlers.add(handler);
|
|
1765
2627
|
return this;
|
|
1766
2628
|
}
|
|
1767
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
1768
|
-
throw new Error(`The event "${
|
|
2629
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2630
|
+
throw new Error(`The event "${event}" is reserved for internal use.`);
|
|
1769
2631
|
}
|
|
1770
2632
|
const typedHandler = handler;
|
|
1771
2633
|
const listeners = this.customEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
@@ -1796,7 +2658,7 @@ var SecureClient = class {
|
|
|
1796
2658
|
this.errorHandlers.delete(handler);
|
|
1797
2659
|
return this;
|
|
1798
2660
|
}
|
|
1799
|
-
if (event === INTERNAL_HANDSHAKE_EVENT) {
|
|
2661
|
+
if (event === INTERNAL_HANDSHAKE_EVENT || event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
1800
2662
|
return this;
|
|
1801
2663
|
}
|
|
1802
2664
|
const listeners = this.customEventHandlers.get(event);
|
|
@@ -1814,6 +2676,38 @@ var SecureClient = class {
|
|
|
1814
2676
|
}
|
|
1815
2677
|
return this;
|
|
1816
2678
|
}
|
|
2679
|
+
onStream(event, handler) {
|
|
2680
|
+
try {
|
|
2681
|
+
if (isReservedEmitEvent(event)) {
|
|
2682
|
+
throw new Error(`The event "${event}" is reserved and cannot be used as a stream event.`);
|
|
2683
|
+
}
|
|
2684
|
+
const listeners = this.streamEventHandlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
2685
|
+
listeners.add(handler);
|
|
2686
|
+
this.streamEventHandlers.set(event, listeners);
|
|
2687
|
+
} catch (error) {
|
|
2688
|
+
this.notifyError(
|
|
2689
|
+
normalizeToError(error, "Failed to register client stream handler.")
|
|
2690
|
+
);
|
|
2691
|
+
}
|
|
2692
|
+
return this;
|
|
2693
|
+
}
|
|
2694
|
+
offStream(event, handler) {
|
|
2695
|
+
try {
|
|
2696
|
+
const listeners = this.streamEventHandlers.get(event);
|
|
2697
|
+
if (!listeners) {
|
|
2698
|
+
return this;
|
|
2699
|
+
}
|
|
2700
|
+
listeners.delete(handler);
|
|
2701
|
+
if (listeners.size === 0) {
|
|
2702
|
+
this.streamEventHandlers.delete(event);
|
|
2703
|
+
}
|
|
2704
|
+
} catch (error) {
|
|
2705
|
+
this.notifyError(
|
|
2706
|
+
normalizeToError(error, "Failed to remove client stream handler.")
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
return this;
|
|
2710
|
+
}
|
|
1817
2711
|
emit(event, data, callbackOrOptions, maybeCallback) {
|
|
1818
2712
|
const ackArgs = resolveAckArguments(callbackOrOptions, maybeCallback);
|
|
1819
2713
|
try {
|
|
@@ -1859,6 +2753,39 @@ var SecureClient = class {
|
|
|
1859
2753
|
return false;
|
|
1860
2754
|
}
|
|
1861
2755
|
}
|
|
2756
|
+
async emitStream(event, source, options) {
|
|
2757
|
+
try {
|
|
2758
|
+
if (isReservedEmitEvent(event)) {
|
|
2759
|
+
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
2760
|
+
}
|
|
2761
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
2762
|
+
throw new Error("Client socket is not connected.");
|
|
2763
|
+
}
|
|
2764
|
+
if (!this.isHandshakeReady()) {
|
|
2765
|
+
throw new Error(
|
|
2766
|
+
`Cannot stream event "${event}" before secure handshake completion.`
|
|
2767
|
+
);
|
|
2768
|
+
}
|
|
2769
|
+
return await transmitChunkedStreamFrames(
|
|
2770
|
+
event,
|
|
2771
|
+
source,
|
|
2772
|
+
options,
|
|
2773
|
+
async (framePayload) => {
|
|
2774
|
+
await this.sendEncryptedEnvelope({
|
|
2775
|
+
event: INTERNAL_STREAM_FRAME_EVENT,
|
|
2776
|
+
data: framePayload
|
|
2777
|
+
});
|
|
2778
|
+
}
|
|
2779
|
+
);
|
|
2780
|
+
} catch (error) {
|
|
2781
|
+
const normalizedError = normalizeToError(
|
|
2782
|
+
error,
|
|
2783
|
+
`Failed to emit chunked stream event "${event}".`
|
|
2784
|
+
);
|
|
2785
|
+
this.notifyError(normalizedError);
|
|
2786
|
+
throw normalizedError;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
1862
2789
|
resolveReconnectConfig(reconnectOptions) {
|
|
1863
2790
|
if (typeof reconnectOptions === "boolean") {
|
|
1864
2791
|
return {
|
|
@@ -1902,6 +2829,24 @@ var SecureClient = class {
|
|
|
1902
2829
|
maxAttempts
|
|
1903
2830
|
};
|
|
1904
2831
|
}
|
|
2832
|
+
resolveSessionResumptionConfig(sessionResumptionOptions) {
|
|
2833
|
+
if (typeof sessionResumptionOptions === "boolean") {
|
|
2834
|
+
return {
|
|
2835
|
+
enabled: sessionResumptionOptions,
|
|
2836
|
+
maxAcceptedTicketTtlMs: DEFAULT_SESSION_TICKET_TTL_MS
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
const maxAcceptedTicketTtlMs = sessionResumptionOptions?.maxAcceptedTicketTtlMs ?? DEFAULT_SESSION_TICKET_TTL_MS;
|
|
2840
|
+
if (!Number.isFinite(maxAcceptedTicketTtlMs) || maxAcceptedTicketTtlMs <= 0) {
|
|
2841
|
+
throw new Error(
|
|
2842
|
+
"Client sessionResumption maxAcceptedTicketTtlMs must be a positive number."
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
return {
|
|
2846
|
+
enabled: sessionResumptionOptions?.enabled ?? DEFAULT_SESSION_RESUMPTION_ENABLED,
|
|
2847
|
+
maxAcceptedTicketTtlMs
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
1905
2850
|
scheduleReconnect() {
|
|
1906
2851
|
if (!this.reconnectConfig.enabled || this.reconnectTimer) {
|
|
1907
2852
|
return;
|
|
@@ -1949,7 +2894,6 @@ var SecureClient = class {
|
|
|
1949
2894
|
socket.on("open", () => {
|
|
1950
2895
|
this.clearReconnectTimer();
|
|
1951
2896
|
this.reconnectAttemptCount = 0;
|
|
1952
|
-
this.sendInternalHandshake();
|
|
1953
2897
|
this.notifyConnect();
|
|
1954
2898
|
});
|
|
1955
2899
|
socket.on("message", (rawData) => {
|
|
@@ -2005,6 +2949,14 @@ var SecureClient = class {
|
|
|
2005
2949
|
void this.handleRpcRequest(decryptedEnvelope.data);
|
|
2006
2950
|
return;
|
|
2007
2951
|
}
|
|
2952
|
+
if (decryptedEnvelope.event === INTERNAL_STREAM_FRAME_EVENT) {
|
|
2953
|
+
this.handleIncomingStreamFrame(decryptedEnvelope.data);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
if (decryptedEnvelope.event === INTERNAL_SESSION_TICKET_EVENT) {
|
|
2957
|
+
this.handleSessionTicket(decryptedEnvelope.data);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2008
2960
|
this.dispatchCustomEvent(decryptedEnvelope.event, decryptedEnvelope.data);
|
|
2009
2961
|
} catch (error) {
|
|
2010
2962
|
this.notifyError(normalizeToError(error, "Failed to process incoming client message."));
|
|
@@ -2015,6 +2967,9 @@ var SecureClient = class {
|
|
|
2015
2967
|
this.socket = null;
|
|
2016
2968
|
this.handshakeState = null;
|
|
2017
2969
|
this.pendingPayloadQueue = [];
|
|
2970
|
+
this.cleanupIncomingStreams(
|
|
2971
|
+
"Client disconnected before stream transfer completed."
|
|
2972
|
+
);
|
|
2018
2973
|
this.rejectPendingRpcRequests(
|
|
2019
2974
|
new Error("Client disconnected before ACK response was received.")
|
|
2020
2975
|
);
|
|
@@ -2061,6 +3016,151 @@ var SecureClient = class {
|
|
|
2061
3016
|
}
|
|
2062
3017
|
}
|
|
2063
3018
|
}
|
|
3019
|
+
cleanupIncomingStreams(reason) {
|
|
3020
|
+
for (const streamState of this.incomingStreams.values()) {
|
|
3021
|
+
streamState.stream.destroy(new Error(reason));
|
|
3022
|
+
}
|
|
3023
|
+
this.incomingStreams.clear();
|
|
3024
|
+
}
|
|
3025
|
+
abortIncomingClientStream(streamId, reason) {
|
|
3026
|
+
const streamState = this.incomingStreams.get(streamId);
|
|
3027
|
+
if (!streamState) {
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
streamState.stream.destroy(new Error(reason));
|
|
3031
|
+
this.incomingStreams.delete(streamId);
|
|
3032
|
+
}
|
|
3033
|
+
dispatchClientStreamEvent(event, stream, info) {
|
|
3034
|
+
const handlers = this.streamEventHandlers.get(event);
|
|
3035
|
+
if (!handlers || handlers.size === 0) {
|
|
3036
|
+
stream.resume();
|
|
3037
|
+
this.notifyError(
|
|
3038
|
+
new Error(`No stream handler is registered for event "${event}" on client.`)
|
|
3039
|
+
);
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
for (const handler of handlers) {
|
|
3043
|
+
try {
|
|
3044
|
+
const handlerResult = handler(stream, info);
|
|
3045
|
+
if (isPromiseLike(handlerResult)) {
|
|
3046
|
+
void Promise.resolve(handlerResult).catch((error) => {
|
|
3047
|
+
this.notifyError(
|
|
3048
|
+
normalizeToError(
|
|
3049
|
+
error,
|
|
3050
|
+
`Client stream handler failed for event ${event}.`
|
|
3051
|
+
)
|
|
3052
|
+
);
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
} catch (error) {
|
|
3056
|
+
this.notifyError(
|
|
3057
|
+
normalizeToError(error, `Client stream handler failed for event ${event}.`)
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
handleIncomingClientStreamStartFrame(framePayload) {
|
|
3063
|
+
if (isReservedEmitEvent(framePayload.event)) {
|
|
3064
|
+
throw new Error(
|
|
3065
|
+
`Reserved event "${framePayload.event}" cannot be used for stream transport.`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (this.incomingStreams.has(framePayload.streamId)) {
|
|
3069
|
+
throw new Error(`Stream ${framePayload.streamId} already exists on client.`);
|
|
3070
|
+
}
|
|
3071
|
+
const stream$1 = new stream.PassThrough();
|
|
3072
|
+
const streamInfo = {
|
|
3073
|
+
streamId: framePayload.streamId,
|
|
3074
|
+
event: framePayload.event,
|
|
3075
|
+
startedAt: Date.now(),
|
|
3076
|
+
...framePayload.metadata !== void 0 ? { metadata: framePayload.metadata } : {},
|
|
3077
|
+
...framePayload.totalBytes !== void 0 ? { totalBytes: framePayload.totalBytes } : {}
|
|
3078
|
+
};
|
|
3079
|
+
this.incomingStreams.set(framePayload.streamId, {
|
|
3080
|
+
info: streamInfo,
|
|
3081
|
+
stream: stream$1,
|
|
3082
|
+
expectedChunkIndex: 0,
|
|
3083
|
+
receivedBytes: 0
|
|
3084
|
+
});
|
|
3085
|
+
this.dispatchClientStreamEvent(framePayload.event, stream$1, streamInfo);
|
|
3086
|
+
}
|
|
3087
|
+
handleIncomingClientStreamChunkFrame(framePayload) {
|
|
3088
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3089
|
+
if (!streamState) {
|
|
3090
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3091
|
+
}
|
|
3092
|
+
if (framePayload.index !== streamState.expectedChunkIndex) {
|
|
3093
|
+
throw new Error(
|
|
3094
|
+
`Out-of-order chunk index for stream ${framePayload.streamId}. Expected ${streamState.expectedChunkIndex}, received ${framePayload.index}.`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
const chunkBuffer = decodeBase64ToBuffer(
|
|
3098
|
+
framePayload.payload,
|
|
3099
|
+
`Stream chunk payload (${framePayload.streamId})`
|
|
3100
|
+
);
|
|
3101
|
+
if (chunkBuffer.length !== framePayload.byteLength) {
|
|
3102
|
+
throw new Error(
|
|
3103
|
+
`Stream ${framePayload.streamId} byteLength mismatch. Expected ${framePayload.byteLength}, received ${chunkBuffer.length}.`
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
streamState.expectedChunkIndex += 1;
|
|
3107
|
+
streamState.receivedBytes += chunkBuffer.length;
|
|
3108
|
+
streamState.stream.write(chunkBuffer);
|
|
3109
|
+
}
|
|
3110
|
+
handleIncomingClientStreamEndFrame(framePayload) {
|
|
3111
|
+
const streamState = this.incomingStreams.get(framePayload.streamId);
|
|
3112
|
+
if (!streamState) {
|
|
3113
|
+
throw new Error(`Stream ${framePayload.streamId} is unknown on client.`);
|
|
3114
|
+
}
|
|
3115
|
+
if (framePayload.chunkCount !== streamState.expectedChunkIndex) {
|
|
3116
|
+
throw new Error(
|
|
3117
|
+
`Stream ${framePayload.streamId} chunkCount mismatch. Expected ${streamState.expectedChunkIndex}, received ${framePayload.chunkCount}.`
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
if (framePayload.totalBytes !== streamState.receivedBytes) {
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`Stream ${framePayload.streamId} totalBytes mismatch. Expected ${streamState.receivedBytes}, received ${framePayload.totalBytes}.`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
if (streamState.info.totalBytes !== void 0 && streamState.info.totalBytes !== streamState.receivedBytes) {
|
|
3126
|
+
throw new Error(
|
|
3127
|
+
`Stream ${framePayload.streamId} violated announced totalBytes (${streamState.info.totalBytes}).`
|
|
3128
|
+
);
|
|
3129
|
+
}
|
|
3130
|
+
streamState.stream.end();
|
|
3131
|
+
this.incomingStreams.delete(framePayload.streamId);
|
|
3132
|
+
}
|
|
3133
|
+
handleIncomingClientStreamAbortFrame(framePayload) {
|
|
3134
|
+
this.abortIncomingClientStream(framePayload.streamId, framePayload.reason);
|
|
3135
|
+
}
|
|
3136
|
+
handleIncomingStreamFrame(data) {
|
|
3137
|
+
let framePayload = null;
|
|
3138
|
+
try {
|
|
3139
|
+
framePayload = parseStreamFramePayload(data);
|
|
3140
|
+
if (framePayload.type === "start") {
|
|
3141
|
+
this.handleIncomingClientStreamStartFrame(framePayload);
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
if (framePayload.type === "chunk") {
|
|
3145
|
+
this.handleIncomingClientStreamChunkFrame(framePayload);
|
|
3146
|
+
return;
|
|
3147
|
+
}
|
|
3148
|
+
if (framePayload.type === "end") {
|
|
3149
|
+
this.handleIncomingClientStreamEndFrame(framePayload);
|
|
3150
|
+
return;
|
|
3151
|
+
}
|
|
3152
|
+
this.handleIncomingClientStreamAbortFrame(framePayload);
|
|
3153
|
+
} catch (error) {
|
|
3154
|
+
const normalizedError = normalizeToError(
|
|
3155
|
+
error,
|
|
3156
|
+
"Failed to process incoming stream frame on client."
|
|
3157
|
+
);
|
|
3158
|
+
if (framePayload) {
|
|
3159
|
+
this.abortIncomingClientStream(framePayload.streamId, normalizedError.message);
|
|
3160
|
+
}
|
|
3161
|
+
this.notifyError(normalizedError);
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
2064
3164
|
notifyConnect() {
|
|
2065
3165
|
for (const handler of this.connectHandlers) {
|
|
2066
3166
|
try {
|
|
@@ -2218,11 +3318,45 @@ var SecureClient = class {
|
|
|
2218
3318
|
}
|
|
2219
3319
|
this.pendingRpcRequests.clear();
|
|
2220
3320
|
}
|
|
3321
|
+
handleSessionTicket(data) {
|
|
3322
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
try {
|
|
3326
|
+
const ticketPayload = parseSessionTicketPayload(data);
|
|
3327
|
+
const now = Date.now();
|
|
3328
|
+
if (ticketPayload.expiresAt <= now) {
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
const ticketTtlMs = ticketPayload.expiresAt - ticketPayload.issuedAt;
|
|
3332
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
3333
|
+
throw new Error("Session ticket TTL exceeds client trust policy.");
|
|
3334
|
+
}
|
|
3335
|
+
const sessionSecret = decodeBase64ToBuffer(
|
|
3336
|
+
ticketPayload.secret,
|
|
3337
|
+
"Session ticket secret"
|
|
3338
|
+
);
|
|
3339
|
+
if (sessionSecret.length !== ENCRYPTION_KEY_LENGTH) {
|
|
3340
|
+
throw new Error("Session ticket secret has invalid length.");
|
|
3341
|
+
}
|
|
3342
|
+
this.sessionTicket = {
|
|
3343
|
+
sessionId: ticketPayload.sessionId,
|
|
3344
|
+
secret: sessionSecret,
|
|
3345
|
+
issuedAt: ticketPayload.issuedAt,
|
|
3346
|
+
expiresAt: ticketPayload.expiresAt
|
|
3347
|
+
};
|
|
3348
|
+
} catch (error) {
|
|
3349
|
+
this.notifyError(normalizeToError(error, "Failed to process session ticket payload."));
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
2221
3352
|
createClientHandshakeState() {
|
|
2222
3353
|
const { ecdh, localPublicKey } = createEphemeralHandshakeState();
|
|
2223
3354
|
return {
|
|
2224
3355
|
ecdh,
|
|
2225
3356
|
localPublicKey,
|
|
3357
|
+
clientHelloSent: false,
|
|
3358
|
+
pendingServerPublicKey: null,
|
|
3359
|
+
resumeAttempt: null,
|
|
2226
3360
|
isReady: false,
|
|
2227
3361
|
sharedSecret: null,
|
|
2228
3362
|
encryptionKey: null
|
|
@@ -2236,15 +3370,171 @@ var SecureClient = class {
|
|
|
2236
3370
|
if (!this.handshakeState) {
|
|
2237
3371
|
throw new Error("Missing client handshake state.");
|
|
2238
3372
|
}
|
|
3373
|
+
if (this.handshakeState.clientHelloSent) {
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
2239
3376
|
this.socket.send(
|
|
2240
3377
|
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
3378
|
+
type: "hello",
|
|
3379
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
2241
3380
|
publicKey: this.handshakeState.localPublicKey
|
|
2242
3381
|
})
|
|
2243
3382
|
);
|
|
3383
|
+
this.handshakeState.clientHelloSent = true;
|
|
2244
3384
|
} catch (error) {
|
|
2245
3385
|
this.notifyError(normalizeToError(error, "Failed to send client handshake payload."));
|
|
2246
3386
|
}
|
|
2247
3387
|
}
|
|
3388
|
+
shouldAttemptSessionResumption() {
|
|
3389
|
+
if (!this.sessionResumptionConfig.enabled) {
|
|
3390
|
+
return false;
|
|
3391
|
+
}
|
|
3392
|
+
const sessionTicket = this.sessionTicket;
|
|
3393
|
+
if (!sessionTicket) {
|
|
3394
|
+
return false;
|
|
3395
|
+
}
|
|
3396
|
+
const now = Date.now();
|
|
3397
|
+
if (sessionTicket.expiresAt <= now) {
|
|
3398
|
+
this.sessionTicket = null;
|
|
3399
|
+
return false;
|
|
3400
|
+
}
|
|
3401
|
+
const ticketTtlMs = sessionTicket.expiresAt - sessionTicket.issuedAt;
|
|
3402
|
+
if (ticketTtlMs > this.sessionResumptionConfig.maxAcceptedTicketTtlMs) {
|
|
3403
|
+
this.sessionTicket = null;
|
|
3404
|
+
return false;
|
|
3405
|
+
}
|
|
3406
|
+
return true;
|
|
3407
|
+
}
|
|
3408
|
+
sendResumeHandshake() {
|
|
3409
|
+
if (!this.socket || this.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
3410
|
+
return false;
|
|
3411
|
+
}
|
|
3412
|
+
if (!this.handshakeState || !this.sessionTicket) {
|
|
3413
|
+
return false;
|
|
3414
|
+
}
|
|
3415
|
+
if (this.handshakeState.clientHelloSent) {
|
|
3416
|
+
return false;
|
|
3417
|
+
}
|
|
3418
|
+
if (this.handshakeState.resumeAttempt?.status === "pending") {
|
|
3419
|
+
return true;
|
|
3420
|
+
}
|
|
3421
|
+
try {
|
|
3422
|
+
const clientNonce = crypto.randomBytes(RESUMPTION_NONCE_LENGTH);
|
|
3423
|
+
const resumedKey = deriveResumedEncryptionKey(this.sessionTicket.secret, clientNonce);
|
|
3424
|
+
const clientProof = createResumeClientProof(
|
|
3425
|
+
this.sessionTicket.secret,
|
|
3426
|
+
this.sessionTicket.sessionId,
|
|
3427
|
+
clientNonce
|
|
3428
|
+
);
|
|
3429
|
+
this.socket.send(
|
|
3430
|
+
serializePlainEnvelope(INTERNAL_HANDSHAKE_EVENT, {
|
|
3431
|
+
type: "resume",
|
|
3432
|
+
protocolVersion: HANDSHAKE_PROTOCOL_VERSION,
|
|
3433
|
+
sessionId: this.sessionTicket.sessionId,
|
|
3434
|
+
clientNonce: clientNonce.toString("base64"),
|
|
3435
|
+
clientProof: clientProof.toString("base64")
|
|
3436
|
+
})
|
|
3437
|
+
);
|
|
3438
|
+
this.handshakeState.resumeAttempt = {
|
|
3439
|
+
status: "pending",
|
|
3440
|
+
sessionId: this.sessionTicket.sessionId,
|
|
3441
|
+
clientNonce,
|
|
3442
|
+
resumedKey
|
|
3443
|
+
};
|
|
3444
|
+
return true;
|
|
3445
|
+
} catch (error) {
|
|
3446
|
+
this.notifyError(normalizeToError(error, "Failed to dispatch resume handshake payload."));
|
|
3447
|
+
this.sessionTicket = null;
|
|
3448
|
+
this.handshakeState.resumeAttempt = null;
|
|
3449
|
+
return false;
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
completeFullHandshake(serverPublicKey) {
|
|
3453
|
+
if (!this.handshakeState) {
|
|
3454
|
+
throw new Error("Missing client handshake state.");
|
|
3455
|
+
}
|
|
3456
|
+
if (this.handshakeState.isReady) {
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
this.sendInternalHandshake();
|
|
3460
|
+
const remotePublicKey = Buffer.from(serverPublicKey, "base64");
|
|
3461
|
+
const sharedSecret = this.handshakeState.ecdh.computeSecret(remotePublicKey);
|
|
3462
|
+
this.handshakeState.sharedSecret = sharedSecret;
|
|
3463
|
+
this.handshakeState.encryptionKey = deriveEncryptionKey(sharedSecret);
|
|
3464
|
+
this.handshakeState.resumeAttempt = null;
|
|
3465
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
3466
|
+
this.handshakeState.isReady = true;
|
|
3467
|
+
void this.flushPendingPayloadQueue();
|
|
3468
|
+
this.notifyReady();
|
|
3469
|
+
}
|
|
3470
|
+
fallbackToFullHandshake() {
|
|
3471
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
if (this.handshakeState.resumeAttempt) {
|
|
3475
|
+
this.handshakeState.resumeAttempt.status = "failed";
|
|
3476
|
+
}
|
|
3477
|
+
const pendingServerPublicKey = this.handshakeState.pendingServerPublicKey;
|
|
3478
|
+
if (pendingServerPublicKey) {
|
|
3479
|
+
this.completeFullHandshake(pendingServerPublicKey);
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
this.sendInternalHandshake();
|
|
3483
|
+
}
|
|
3484
|
+
handleServerHelloHandshake(payload) {
|
|
3485
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
3486
|
+
return;
|
|
3487
|
+
}
|
|
3488
|
+
this.handshakeState.pendingServerPublicKey = payload.publicKey;
|
|
3489
|
+
if (this.shouldAttemptSessionResumption() && this.sendResumeHandshake()) {
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
this.completeFullHandshake(payload.publicKey);
|
|
3493
|
+
}
|
|
3494
|
+
handleResumeAckHandshake(payload) {
|
|
3495
|
+
if (!this.handshakeState || this.handshakeState.isReady) {
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
const resumeAttempt = this.handshakeState.resumeAttempt;
|
|
3499
|
+
if (!resumeAttempt || resumeAttempt.status !== "pending") {
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
if (!payload.ok) {
|
|
3503
|
+
this.sessionTicket = null;
|
|
3504
|
+
this.fallbackToFullHandshake();
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
if (payload.sessionId !== resumeAttempt.sessionId || !payload.serverProof) {
|
|
3508
|
+
this.sessionTicket = null;
|
|
3509
|
+
this.fallbackToFullHandshake();
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
try {
|
|
3513
|
+
const receivedServerProof = decodeBase64ToBuffer(
|
|
3514
|
+
payload.serverProof,
|
|
3515
|
+
"Handshake resume-ack serverProof"
|
|
3516
|
+
);
|
|
3517
|
+
const expectedServerProof = createResumeServerProof(
|
|
3518
|
+
resumeAttempt.resumedKey,
|
|
3519
|
+
resumeAttempt.sessionId,
|
|
3520
|
+
resumeAttempt.clientNonce
|
|
3521
|
+
);
|
|
3522
|
+
if (!equalsConstantTime(receivedServerProof, expectedServerProof)) {
|
|
3523
|
+
throw new Error("Resume server proof validation failed.");
|
|
3524
|
+
}
|
|
3525
|
+
this.handshakeState.sharedSecret = resumeAttempt.resumedKey;
|
|
3526
|
+
this.handshakeState.encryptionKey = resumeAttempt.resumedKey;
|
|
3527
|
+
this.handshakeState.pendingServerPublicKey = null;
|
|
3528
|
+
resumeAttempt.status = "accepted";
|
|
3529
|
+
this.handshakeState.isReady = true;
|
|
3530
|
+
void this.flushPendingPayloadQueue();
|
|
3531
|
+
this.notifyReady();
|
|
3532
|
+
} catch (error) {
|
|
3533
|
+
this.notifyError(normalizeToError(error, "Failed to verify resume server proof."));
|
|
3534
|
+
this.sessionTicket = null;
|
|
3535
|
+
this.fallbackToFullHandshake();
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
2248
3538
|
handleInternalHandshake(data) {
|
|
2249
3539
|
try {
|
|
2250
3540
|
const payload = parseHandshakePayload(data);
|
|
@@ -2254,13 +3544,15 @@ var SecureClient = class {
|
|
|
2254
3544
|
if (this.handshakeState.isReady) {
|
|
2255
3545
|
return;
|
|
2256
3546
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
3547
|
+
if (payload.type === "hello") {
|
|
3548
|
+
this.handleServerHelloHandshake(payload);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
if (payload.type === "resume-ack") {
|
|
3552
|
+
this.handleResumeAckHandshake(payload);
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
throw new Error("SecureClient received unexpected resume request handshake payload.");
|
|
2264
3556
|
} catch (error) {
|
|
2265
3557
|
this.notifyError(normalizeToError(error, "Failed to complete client handshake."));
|
|
2266
3558
|
}
|