@peers-app/peers-sdk 0.25.6 → 0.25.7
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/dist/data/devices.d.ts +44 -2
- package/dist/data/devices.js +16 -1
- package/dist/device/binary-peer-connection-v2.d.ts +11 -2
- package/dist/device/binary-peer-connection-v2.js +150 -18
- package/dist/device/binary-peer-connection-v2.test.js +217 -5
- package/dist/device/connection.d.ts +79 -9
- package/dist/device/connection.js +185 -26
- package/dist/device/connection.test.js +224 -0
- package/dist/device/device.d.ts +53 -13
- package/dist/device/device.js +55 -2
- package/dist/device/frame-cipher.d.ts +133 -0
- package/dist/device/frame-cipher.js +212 -0
- package/dist/device/frame-cipher.test.d.ts +1 -0
- package/dist/device/frame-cipher.test.js +219 -0
- package/dist/device/socket.type.d.ts +42 -1
- package/dist/device/socket.type.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.d.ts +19 -0
- package/dist/keys.js +28 -0
- package/dist/rpc-types.d.ts +2 -0
- package/package.json +2 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const nacl = require("tweetnacl");
|
|
3
4
|
const keys_1 = require("../keys");
|
|
4
5
|
const utils_1 = require("../utils");
|
|
5
6
|
const binary_peer_connection_v2_1 = require("./binary-peer-connection-v2");
|
|
6
7
|
const device_1 = require("./device");
|
|
8
|
+
const frame_cipher_1 = require("./frame-cipher");
|
|
7
9
|
// Mock binary peer for testing - simulates WebRTC peers
|
|
8
10
|
class MockBinaryPeer {
|
|
9
11
|
dataHandlers = [];
|
|
@@ -12,6 +14,8 @@ class MockBinaryPeer {
|
|
|
12
14
|
otherPeer;
|
|
13
15
|
_bufferedAmount = 0;
|
|
14
16
|
destroyed = false;
|
|
17
|
+
/** Every frame handed to send(), in order (the "wire tap"). */
|
|
18
|
+
sent = [];
|
|
15
19
|
on(event, handler) {
|
|
16
20
|
if (event === "data") {
|
|
17
21
|
this.dataHandlers.push(handler);
|
|
@@ -24,12 +28,13 @@ class MockBinaryPeer {
|
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
send(data) {
|
|
31
|
+
const bytes = typeof data === "string"
|
|
32
|
+
? Buffer.from(data)
|
|
33
|
+
: Buffer.isBuffer(data)
|
|
34
|
+
? data
|
|
35
|
+
: Buffer.from(data);
|
|
36
|
+
this.sent.push(bytes);
|
|
27
37
|
if (this.otherPeer) {
|
|
28
|
-
const bytes = typeof data === "string"
|
|
29
|
-
? Buffer.from(data)
|
|
30
|
-
: Buffer.isBuffer(data)
|
|
31
|
-
? data
|
|
32
|
-
: Buffer.from(data);
|
|
33
38
|
// Simulate async delivery
|
|
34
39
|
setImmediate(() => {
|
|
35
40
|
const handlers = this.otherPeer?.dataHandlers;
|
|
@@ -410,6 +415,213 @@ describe("binary-peer-connection-v2", () => {
|
|
|
410
415
|
rPeer.destroy();
|
|
411
416
|
});
|
|
412
417
|
});
|
|
418
|
+
describe("frame cipher", () => {
|
|
419
|
+
function keyPair() {
|
|
420
|
+
const ab = nacl.randomBytes(32);
|
|
421
|
+
const ba = nacl.randomBytes(32);
|
|
422
|
+
return {
|
|
423
|
+
a: { sendKey: ab.slice(), receiveKey: ba.slice() },
|
|
424
|
+
b: { sendKey: ba.slice(), receiveKey: ab.slice() },
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function armedPair(protocol, connectionId = "conn") {
|
|
428
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
429
|
+
const socketA = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)(connectionId, iPeer, protocol);
|
|
430
|
+
const socketB = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)(connectionId, rPeer, protocol);
|
|
431
|
+
const keys = keyPair();
|
|
432
|
+
socketA.armFrameCipher?.(keys.a);
|
|
433
|
+
socketB.armFrameCipher?.(keys.b);
|
|
434
|
+
return { iPeer, rPeer, socketA, socketB };
|
|
435
|
+
}
|
|
436
|
+
function emit(socket, eventName, args) {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
socket.emit(eventName, args, (err, res) => (err ? reject(err) : resolve(res)));
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
for (const protocol of ["ws", "wrtc"]) {
|
|
442
|
+
it(`round-trips RPC and streams when armed on ${protocol}, sealing every frame`, async () => {
|
|
443
|
+
const { iPeer, rPeer, socketA, socketB } = await armedPair(protocol);
|
|
444
|
+
expect(socketA.frameCipherArmed).toBe(true);
|
|
445
|
+
expect(socketB.frameCipherArmed).toBe(true);
|
|
446
|
+
const marker = `RPC-MARKER-${(0, utils_1.newid)()}`;
|
|
447
|
+
socketB.on("echo", (args, callback) => {
|
|
448
|
+
callback(null, { got: args.marker, reply: `${marker}-reply` });
|
|
449
|
+
});
|
|
450
|
+
const result = await emit(socketA, "echo", { marker });
|
|
451
|
+
expect(result).toEqual({ got: marker, reply: `${marker}-reply` });
|
|
452
|
+
const streamMarker = nacl.randomBytes(64);
|
|
453
|
+
const received = new Promise((resolve) => socketB.onRawBytes?.("s", (d) => resolve(d)));
|
|
454
|
+
await socketA.sendRawBytes?.("s", streamMarker);
|
|
455
|
+
expect(Buffer.from(await received)).toEqual(Buffer.from(streamMarker));
|
|
456
|
+
// Wire tap: every frame is FRAME_ENC and neither marker appears on the wire.
|
|
457
|
+
const wire = [...iPeer.sent, ...rPeer.sent];
|
|
458
|
+
expect(wire.length).toBeGreaterThanOrEqual(3);
|
|
459
|
+
for (const frame of wire) {
|
|
460
|
+
expect(frame[0]).toBe(frame_cipher_1.FRAME_ENC);
|
|
461
|
+
expect(frame.indexOf(Buffer.from(marker))).toBe(-1);
|
|
462
|
+
expect(frame.indexOf(Buffer.from(streamMarker))).toBe(-1);
|
|
463
|
+
}
|
|
464
|
+
iPeer.destroy();
|
|
465
|
+
rPeer.destroy();
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
it("seals an RPC reply that is sent after the local peer has already closed", async () => {
|
|
469
|
+
const { iPeer, rPeer, socketA, socketB } = await armedPair("wrtc");
|
|
470
|
+
const sentBefore = rPeer.sent.length;
|
|
471
|
+
socketB.on("echo", async (_args, callback) => {
|
|
472
|
+
// Tear down before the reply, as a handshake failure does when the
|
|
473
|
+
// confirmation handler resumes in a later microtask.
|
|
474
|
+
rPeer.destroy();
|
|
475
|
+
expect(socketB.connected).toBe(false);
|
|
476
|
+
await Promise.resolve();
|
|
477
|
+
callback(null, { ok: true });
|
|
478
|
+
});
|
|
479
|
+
await expect(emit(socketA, "echo", { n: 1 })).resolves.toEqual({ ok: true });
|
|
480
|
+
const replies = rPeer.sent.slice(sentBefore);
|
|
481
|
+
expect(replies.length).toBeGreaterThan(0);
|
|
482
|
+
for (const frame of replies) {
|
|
483
|
+
expect(frame[0]).toBe(frame_cipher_1.FRAME_ENC);
|
|
484
|
+
}
|
|
485
|
+
expect(socketB.frameCipherArmed).toBe(false);
|
|
486
|
+
iPeer.destroy();
|
|
487
|
+
});
|
|
488
|
+
it("keeps chunked RPC and stream frames within maxFramePayload including overhead", async () => {
|
|
489
|
+
const { iPeer, rPeer, socketA, socketB } = await armedPair("wrtc");
|
|
490
|
+
const maxFrame = 64 * 1024;
|
|
491
|
+
const large = new Uint8Array(200 * 1024);
|
|
492
|
+
for (let i = 0; i < large.length; i++)
|
|
493
|
+
large[i] = i % 256;
|
|
494
|
+
socketB.on("big", (args, callback) => {
|
|
495
|
+
callback(null, args.length);
|
|
496
|
+
});
|
|
497
|
+
expect(await emit(socketA, "big", large)).toBe(large.length);
|
|
498
|
+
const received = new Promise((resolve) => socketB.onRawBytes?.("big-stream", (d) => resolve(d)));
|
|
499
|
+
await socketA.sendRawBytes?.("big-stream", large);
|
|
500
|
+
expect((await received).length).toBe(large.length);
|
|
501
|
+
expect(iPeer.sent.length).toBeGreaterThan(6);
|
|
502
|
+
for (const frame of iPeer.sent) {
|
|
503
|
+
expect(frame.length).toBeLessThanOrEqual(maxFrame);
|
|
504
|
+
expect(frame[0]).toBe(frame_cipher_1.FRAME_ENC);
|
|
505
|
+
}
|
|
506
|
+
// Bytes the sealed frames are budgeted for are actually used: at least one full frame.
|
|
507
|
+
expect(iPeer.sent.some((f) => f.length === maxFrame)).toBe(true);
|
|
508
|
+
iPeer.destroy();
|
|
509
|
+
rPeer.destroy();
|
|
510
|
+
});
|
|
511
|
+
it("closes the connection when a plaintext frame arrives after arming", async () => {
|
|
512
|
+
const { iPeer } = await getMockPeerPair();
|
|
513
|
+
const socket = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", iPeer, "ws");
|
|
514
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
515
|
+
try {
|
|
516
|
+
socket.armFrameCipher?.(keyPair().a);
|
|
517
|
+
iPeer.receive(new Uint8Array([0x00, 0, 0, 0, 0]));
|
|
518
|
+
expect(iPeer.destroyed).toBe(true);
|
|
519
|
+
expect(socket.connected).toBe(false);
|
|
520
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("plaintext frame"));
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
warn.mockRestore();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
it("closes the connection when a sealed frame arrives before arming", async () => {
|
|
527
|
+
const { iPeer } = await getMockPeerPair();
|
|
528
|
+
(0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", iPeer, "ws");
|
|
529
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
530
|
+
try {
|
|
531
|
+
iPeer.receive(new Uint8Array([frame_cipher_1.FRAME_ENC, ...new Uint8Array(20)]));
|
|
532
|
+
expect(iPeer.destroyed).toBe(true);
|
|
533
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("before frame cipher"));
|
|
534
|
+
}
|
|
535
|
+
finally {
|
|
536
|
+
warn.mockRestore();
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
it("closes the connection when a sealed frame fails authentication", async () => {
|
|
540
|
+
const { iPeer, rPeer, socketA } = await armedPair("ws");
|
|
541
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
542
|
+
try {
|
|
543
|
+
// Re-arm B with unrelated keys so A's frames fail to open.
|
|
544
|
+
const socketB = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", rPeer, "ws");
|
|
545
|
+
socketB.armFrameCipher?.(keyPair().b);
|
|
546
|
+
socketA.emit("x", {}, () => { });
|
|
547
|
+
await (0, utils_1.sleep)(20);
|
|
548
|
+
expect(rPeer.destroyed).toBe(true);
|
|
549
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("failed authentication"));
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
warn.mockRestore();
|
|
553
|
+
iPeer.destroy();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
it("drops plaintext stream frames when plaintext streams are disallowed and not armed", async () => {
|
|
557
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
558
|
+
const socketA = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", iPeer, "ws");
|
|
559
|
+
const socketB = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", rPeer, "ws");
|
|
560
|
+
socketB.setPlaintextStreamsAllowed?.(false);
|
|
561
|
+
let delivered = 0;
|
|
562
|
+
socketB.onRawBytes?.("gated", () => delivered++);
|
|
563
|
+
await socketA.sendRawBytes?.("gated", new Uint8Array([1, 2, 3]));
|
|
564
|
+
await (0, utils_1.sleep)(20);
|
|
565
|
+
expect(delivered).toBe(0);
|
|
566
|
+
expect(rPeer.destroyed).toBe(false);
|
|
567
|
+
// Plaintext RPC still works before the handshake.
|
|
568
|
+
socketB.on("ping", (_a, cb) => cb(null, "pong"));
|
|
569
|
+
expect(await emit(socketA, "ping", {})).toBe("pong");
|
|
570
|
+
// Re-allowing (legacy peer path) delivers again.
|
|
571
|
+
socketB.setPlaintextStreamsAllowed?.(true);
|
|
572
|
+
await socketA.sendRawBytes?.("gated", new Uint8Array([4]));
|
|
573
|
+
await (0, utils_1.sleep)(20);
|
|
574
|
+
expect(delivered).toBe(1);
|
|
575
|
+
iPeer.destroy();
|
|
576
|
+
rPeer.destroy();
|
|
577
|
+
});
|
|
578
|
+
it("arms send after the reply frame when the callback carries armFrameCipherAfter", async () => {
|
|
579
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
580
|
+
const client = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", iPeer, "ws");
|
|
581
|
+
const server = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", rPeer, "ws");
|
|
582
|
+
const keys = keyPair();
|
|
583
|
+
server.on("completeHandshake", (_args, callback) => {
|
|
584
|
+
// Server: arm receive now, send after this reply (as Connection does).
|
|
585
|
+
server.armFrameCipher?.(keys.b, "receive");
|
|
586
|
+
expect(server.frameCipherArmed).toBe(false);
|
|
587
|
+
callback(null, { ok: true }, { armFrameCipherAfter: keys.b });
|
|
588
|
+
});
|
|
589
|
+
server.on("afterwards", (args, cb) => cb(null, `sealed:${args}`));
|
|
590
|
+
const reply = await emit(client, "completeHandshake", {});
|
|
591
|
+
expect(reply).toEqual({ ok: true });
|
|
592
|
+
// The reply itself was plaintext...
|
|
593
|
+
expect(rPeer.sent.length).toBe(1);
|
|
594
|
+
expect(rPeer.sent[0][0]).not.toBe(frame_cipher_1.FRAME_ENC);
|
|
595
|
+
// ...and the server is now fully armed.
|
|
596
|
+
expect(server.frameCipherArmed).toBe(true);
|
|
597
|
+
// Client arms both on receipt and everything after is sealed both ways.
|
|
598
|
+
client.armFrameCipher?.(keys.a);
|
|
599
|
+
expect(await emit(client, "afterwards", "x")).toBe("sealed:x");
|
|
600
|
+
expect(iPeer.sent[iPeer.sent.length - 1][0]).toBe(frame_cipher_1.FRAME_ENC);
|
|
601
|
+
expect(rPeer.sent[rPeer.sent.length - 1][0]).toBe(frame_cipher_1.FRAME_ENC);
|
|
602
|
+
iPeer.destroy();
|
|
603
|
+
rPeer.destroy();
|
|
604
|
+
});
|
|
605
|
+
it("arms send after the last chunk of a chunked reply", async () => {
|
|
606
|
+
const { iPeer, rPeer } = await getMockPeerPair();
|
|
607
|
+
const client = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", iPeer, "wrtc");
|
|
608
|
+
const server = (0, binary_peer_connection_v2_1.createBinaryPeerSocket)("conn", rPeer, "wrtc");
|
|
609
|
+
const keys = keyPair();
|
|
610
|
+
const bigReply = new Uint8Array(150 * 1024);
|
|
611
|
+
server.on("hs", (_args, callback) => {
|
|
612
|
+
server.armFrameCipher?.(keys.b, "receive");
|
|
613
|
+
callback(null, bigReply, { armFrameCipherAfter: keys.b });
|
|
614
|
+
});
|
|
615
|
+
const reply = await emit(client, "hs", {});
|
|
616
|
+
expect(reply.length).toBe(bigReply.length);
|
|
617
|
+
expect(rPeer.sent.length).toBeGreaterThan(1);
|
|
618
|
+
for (const frame of rPeer.sent)
|
|
619
|
+
expect(frame[0]).not.toBe(frame_cipher_1.FRAME_ENC);
|
|
620
|
+
expect(server.frameCipherArmed).toBe(true);
|
|
621
|
+
iPeer.destroy();
|
|
622
|
+
rPeer.destroy();
|
|
623
|
+
});
|
|
624
|
+
});
|
|
413
625
|
describe("stats tracking", () => {
|
|
414
626
|
it("should track bytes sent and received", async () => {
|
|
415
627
|
const { iPeer, rPeer } = await getMockPeerPair();
|
|
@@ -4,38 +4,71 @@ import type { Device } from "./device";
|
|
|
4
4
|
import { type ISocket, TrustLevel } from "./socket.type";
|
|
5
5
|
/** Resolves the trust assigned to an authenticated remote device identity. */
|
|
6
6
|
export type GetTrustLevel = (deviceInfo: IDeviceInfo, registerNew?: boolean) => Promise<TrustLevel>;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Authenticated RPC connection between two Peers devices.
|
|
9
|
+
*
|
|
10
|
+
* The handshake (`doHandshake` on the client, `completeHandshake` on the
|
|
11
|
+
* server) authenticates both identities with signed, boxed messages. When both
|
|
12
|
+
* sockets support it, the handshake also agrees on per-connection frame-cipher
|
|
13
|
+
* keys (`frame-cipher.ts`) and arms the socket, after which every frame on the
|
|
14
|
+
* wire is sealed and the legacy per-message sign+box envelope is skipped.
|
|
15
|
+
* Against a peer that does not advertise `frameCrypto` the connection falls
|
|
16
|
+
* back to the legacy behaviour and logs a warning.
|
|
17
|
+
*/
|
|
8
18
|
export declare class Connection {
|
|
9
19
|
readonly localDevice: Device;
|
|
10
20
|
readonly localDeviceServerAddresses?: string[] | undefined;
|
|
11
21
|
protected readonly getTrustLevel: GetTrustLevel;
|
|
12
22
|
private _verified;
|
|
13
23
|
private _remoteDeviceInfo;
|
|
24
|
+
/**
|
|
25
|
+
* @deprecated Legacy transport classification, only consulted when the frame
|
|
26
|
+
* cipher is not armed. Removed once legacy peers are no longer supported.
|
|
27
|
+
*/
|
|
14
28
|
secureLocal: boolean;
|
|
29
|
+
/** @deprecated See {@link Connection.secureLocal}. */
|
|
15
30
|
secureRemote: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Legacy per-message encryption override. Ignored once the frame cipher is
|
|
33
|
+
* armed. `"dont-encrypt-traffic-after-handshake"` is honoured only under
|
|
34
|
+
* `NODE_ENV=test`.
|
|
35
|
+
* @deprecated Removed with the legacy path.
|
|
36
|
+
*/
|
|
16
37
|
overrideEncryption?: "encrypt-traffic-regardless-of-protocol" | "dont-encrypt-traffic-after-handshake";
|
|
38
|
+
/** Trust assigned to the remote identity once the handshake completes. */
|
|
17
39
|
trustLevel: TrustLevel;
|
|
40
|
+
/** Server-side hook fired shortly after a successful handshake. */
|
|
18
41
|
onHandshakeComplete: ((deviceInfo: IDeviceInfo) => void) | undefined;
|
|
42
|
+
/** RPC timeout for {@link Connection.emit}. */
|
|
19
43
|
timeoutMs: number;
|
|
44
|
+
/** Preferred chunk size for callers streaming raw bytes over this connection. */
|
|
20
45
|
maxChunkSize: number;
|
|
46
|
+
/** Maximum accepted clock skew between the two devices' handshake timestamps. */
|
|
21
47
|
handshakeTimestampToleranceMs: number;
|
|
22
48
|
private _connectionAddress;
|
|
49
|
+
/** Normalized server address this connection was established against. */
|
|
23
50
|
get connectionAddress(): string | undefined;
|
|
51
|
+
/** Underlying transport socket. */
|
|
24
52
|
readonly socket: ISocket;
|
|
53
|
+
private pendingClientHandshake;
|
|
54
|
+
/** Server side: keys to arm for sending right after the handshake response frame. */
|
|
55
|
+
private pendingSendArm;
|
|
25
56
|
constructor(socket: ISocket, localDevice: Device, localDeviceServerAddresses?: string[] | undefined, getTrustLevel?: GetTrustLevel);
|
|
57
|
+
/** True once the remote identity has been authenticated by the handshake. */
|
|
26
58
|
get verified(): boolean;
|
|
59
|
+
/** True when every frame on this connection is sealed by the frame cipher. */
|
|
60
|
+
get frameCipherArmed(): boolean;
|
|
27
61
|
/**
|
|
28
|
-
*
|
|
62
|
+
* Whether to apply the legacy per-message envelope (sign + box) to RPC payloads.
|
|
29
63
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
*
|
|
35
|
-
* Encryption is APPLIED when:
|
|
36
|
-
* - Insecure WebSockets (ws/http): Data travels over unencrypted channel
|
|
64
|
+
* Always false once the frame cipher is armed: the frame layer already gives
|
|
65
|
+
* confidentiality, integrity, and sender binding for every frame. Otherwise
|
|
66
|
+
* (legacy peer) the transport classification below applies:
|
|
67
|
+
* - skipped when both sides agreed the transport is secure (`secureLocal && secureRemote`)
|
|
68
|
+
* - applied on plain `ws` / `http`
|
|
37
69
|
*/
|
|
38
70
|
get encryptTraffic(): boolean;
|
|
71
|
+
/** Copy of the authenticated remote device identity (empty before the handshake). */
|
|
39
72
|
get remoteDeviceInfo(): {
|
|
40
73
|
publicKey: string;
|
|
41
74
|
publicBoxKey: string;
|
|
@@ -44,7 +77,11 @@ export declare class Connection {
|
|
|
44
77
|
userName?: string | undefined;
|
|
45
78
|
deviceName?: string | undefined;
|
|
46
79
|
};
|
|
80
|
+
/** Socket ID shared by both ends; also the frame-cipher AAD and HKDF salt. */
|
|
47
81
|
get connectionId(): string;
|
|
82
|
+
private get supportsFrameCrypto();
|
|
83
|
+
private warnLegacyPeer;
|
|
84
|
+
private readonly handleCompleteHandshakeRPC;
|
|
48
85
|
/**
|
|
49
86
|
* Get connection throughput stats with calculated rates.
|
|
50
87
|
* Returns bytes/messages sent/received and both average and recent rates.
|
|
@@ -63,18 +100,51 @@ export declare class Connection {
|
|
|
63
100
|
sendRateMBps: number;
|
|
64
101
|
receiveRateMBps: number;
|
|
65
102
|
} | null;
|
|
103
|
+
/** Registers `fn` as an RPC handler under `name` (defaults to `fn.name`). */
|
|
66
104
|
exposeRPC<T extends Function>(name: string, fn: T): void;
|
|
67
105
|
exposeRPC<T extends Function>(fn: T): void;
|
|
106
|
+
/** Calls a remote RPC handler and resolves with its result (rejects on error or timeout). */
|
|
68
107
|
emit<T = any>(eventName: string, ...args: any): Promise<T>;
|
|
108
|
+
/**
|
|
109
|
+
* Registers an RPC handler. Events other than the handshake set are rejected
|
|
110
|
+
* until the connection is verified; payloads use the legacy envelope only
|
|
111
|
+
* when {@link Connection.encryptTraffic} is true.
|
|
112
|
+
*/
|
|
69
113
|
on(eventName: string, handler: Function): void;
|
|
114
|
+
/** Removes RPC handlers for `eventName`, or all handlers when omitted. */
|
|
70
115
|
removeAllListeners(eventName?: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Clears the authenticated state so a new handshake can run. Does not disarm
|
|
118
|
+
* an already-armed frame cipher: the socket stays sealed with the current
|
|
119
|
+
* keys and a subsequent handshake replaces them.
|
|
120
|
+
*/
|
|
71
121
|
reset(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Client side: builds the signed handshake (advertising frame-cipher support
|
|
124
|
+
* with a fresh ephemeral key when the socket can arm one) and boxes it to the
|
|
125
|
+
* remote device.
|
|
126
|
+
*/
|
|
72
127
|
initiateHandshake(serverAddress: string, remoteDeviceInfo?: IDeviceInfo): IDataBox;
|
|
128
|
+
/**
|
|
129
|
+
* Server side: verifies the client's handshake, resolves trust, and returns
|
|
130
|
+
* the (unsigned) response for the caller to sign and box. When the client
|
|
131
|
+
* offered `frameCrypto` and this socket can arm a cipher, derives the frame
|
|
132
|
+
* keys, arms the receive direction immediately, and schedules the send
|
|
133
|
+
* direction to arm right after the response frame (see
|
|
134
|
+
* {@link Connection.handleCompleteHandshakeRPC}).
|
|
135
|
+
*/
|
|
73
136
|
completeHandshake(boxedHandshake: IDataBox): Promise<IDeviceHandshake>;
|
|
137
|
+
/**
|
|
138
|
+
* Client side: runs the full handshake against `remoteAddress`. On success
|
|
139
|
+
* the remote identity is verified and, if both sockets support it, the
|
|
140
|
+
* frame cipher is armed in both directions before this resolves.
|
|
141
|
+
*/
|
|
74
142
|
doHandshake(remoteAddress: string): Promise<IDeviceHandshake>;
|
|
75
143
|
private closeLocal;
|
|
144
|
+
/** Round-trip time in milliseconds. @throws if the connection is not verified */
|
|
76
145
|
ping(): Promise<number>;
|
|
77
146
|
private closed;
|
|
147
|
+
/** Notifies the peer (best effort, 100 ms) then tears down the local side. */
|
|
78
148
|
close(): Promise<void>;
|
|
79
149
|
}
|
|
80
150
|
/** Normalizes a peer address for transport and security comparisons. */
|