@openlv/signaling 0.1.3 → 0.3.1
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/gundb/index.d.ts +3 -0
- package/dist/gundb/index.js +3 -0
- package/dist/gundb/index.js.map +1 -1
- package/dist/handshake.d.ts +12 -0
- package/dist/handshake.js +15 -0
- package/dist/handshake.js.map +1 -0
- package/dist/handshake.test.js +204 -67
- package/dist/handshake.test.js.map +1 -1
- package/dist/index.d.ts +20 -23
- package/dist/index.js +92 -200
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +22 -19
- package/dist/index.test.js.map +1 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.js +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/messages.test.js +1 -1
- package/dist/messages.test.js.map +1 -1
- package/dist/mqtt/index.js +4 -2
- package/dist/mqtt/index.js.map +1 -1
- package/package.json +2 -7
- package/dist/dynamic.d.ts +0 -6
- package/dist/dynamic.js +0 -23
- package/dist/dynamic.js.map +0 -1
package/dist/gundb/index.d.ts
CHANGED
|
@@ -2,5 +2,8 @@ import type { SignalingProtocol } from "../protocol.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* GunDB Signaling Layer
|
|
4
4
|
* https://openlv.sh/api/signaling/gun
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Public gun relays drop frames often enough that a handshake
|
|
7
|
+
* fails regularly. Kept until a third backend replaces it.
|
|
5
8
|
*/
|
|
6
9
|
export declare const gundb: SignalingProtocol;
|
package/dist/gundb/index.js
CHANGED
|
@@ -5,6 +5,9 @@ import { log } from "../utils/log.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* GunDB Signaling Layer
|
|
7
7
|
* https://openlv.sh/api/signaling/gun
|
|
8
|
+
*
|
|
9
|
+
* @deprecated Public gun relays drop frames often enough that a handshake
|
|
10
|
+
* fails regularly. Kept until a third backend replaces it.
|
|
8
11
|
*/
|
|
9
12
|
export const gundb = ({ topic, url }) => {
|
|
10
13
|
const endpoint = url || "wss://try.axe.eco/gun";
|
package/dist/gundb/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gundb/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,GAAG,EAAE,EAAgC,MAAM,KAAK,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gundb/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,GAAG,EAAE,EAAgC,MAAM,KAAK,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;IACzD,MAAM,QAAQ,GAAG,GAAG,IAAI,uBAAuB,CAAC;IAChD,IAAI,UAAoC,CAAC;IAEzC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,OAAO;QACb,KAAK,CAAC,KAAK;YACT,GAAG,CAAC,mBAAmB,CAAC,CAAC;YACzB,MAAM,CAAC,GAAG,IAAK,GAAuB,CAAC;gBACrC,KAAK,EAAE,CAAC,QAAQ,CAAC;gBACjB,IAAI,EAAE,SAAS;gBACf,YAAY,EAAE,SAAS;aACxB,CAAC,CAAC;YAEH,UAAU,GAAG,CAAC,CAAC;YAEf,MAAM,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;YAC7B,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,2BAA2B,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACjD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE;gBAChC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,CAAC;gBAErC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;gBAEtC,IAAI,CAAC,OAAO;oBAAE,OAAO;gBAErB,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;IACnB,MAAM,EAAE,OAAO;CAChB,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport Gun, { type IGun, type IGunInstance } from \"gun\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * GunDB Signaling Layer\n * https://openlv.sh/api/signaling/gun\n *\n * @deprecated Public gun relays drop frames often enough that a handshake\n * fails regularly. Kept until a third backend replaces it.\n */\nexport const gundb: SignalingProtocol = ({ topic, url }) => {\n const endpoint = url || \"wss://try.axe.eco/gun\";\n let connection: IGunInstance | undefined;\n\n return createSignalingLayer({\n type: \"gundb\",\n async setup() {\n log(\"GUNDB: Setting up\");\n const x = new (Gun as unknown as IGun)({\n peers: [endpoint],\n file: undefined,\n localStorage: undefined,\n });\n\n connection = x;\n\n await connection?.get(topic);\n },\n teardown() {\n connection?.get(topic).off();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"GUNDB: Publishing message\", topic, payload);\n connection.get(topic).put({ data: payload });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n connection.get(topic).on((data) => {\n log(\"GUNDB: Received message\", data);\n\n const message = data.data?.toString();\n\n if (!message) return;\n\n handler(message);\n });\n },\n });\n};\n\nObject.assign(gundb, {\n __name: \"gundb\",\n});\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const XR_PREFIX = "x";
|
|
2
|
+
export declare const XR_H_PREFIX = "h";
|
|
3
|
+
export type SignalingPrefix = typeof XR_PREFIX | typeof XR_H_PREFIX;
|
|
4
|
+
export type SignalingRecipient = "h" | "c";
|
|
5
|
+
export declare const HANDSHAKE_TIMEOUT_MS = 30000;
|
|
6
|
+
export declare const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
|
|
7
|
+
export declare const composeFrame: (prefix: SignalingPrefix, recipient: SignalingRecipient, ciphertext: string) => string;
|
|
8
|
+
export declare const parseFrame: (frame: string) => {
|
|
9
|
+
prefix: SignalingPrefix;
|
|
10
|
+
recipient: SignalingRecipient;
|
|
11
|
+
ciphertext: string;
|
|
12
|
+
} | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const XR_PREFIX = "x";
|
|
2
|
+
export const XR_H_PREFIX = "h";
|
|
3
|
+
export const HANDSHAKE_TIMEOUT_MS = 30000;
|
|
4
|
+
export const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
|
|
5
|
+
export const composeFrame = (prefix, recipient, ciphertext) => prefix + recipient + ciphertext;
|
|
6
|
+
export const parseFrame = (frame) => {
|
|
7
|
+
const prefix = frame.slice(0, 1);
|
|
8
|
+
if (prefix !== XR_PREFIX && prefix !== XR_H_PREFIX)
|
|
9
|
+
return;
|
|
10
|
+
const recipient = frame.slice(1, 2);
|
|
11
|
+
if (recipient !== "h" && recipient !== "c")
|
|
12
|
+
return;
|
|
13
|
+
return { prefix, recipient, ciphertext: frame.slice(2) };
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAI/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;AAEjD,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,MAAuB,EACvB,SAA6B,EAC7B,UAAkB,EACV,EAAE,CAAC,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE7C,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,KAAa,EACgF,EAAE;IAC/F,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEjC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO;IAE3D,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEpC,IAAI,SAAS,KAAK,GAAG,IAAI,SAAS,KAAK,GAAG;QAAE,OAAO;IAEnD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3D,CAAC,CAAC","sourcesContent":["export const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\nexport type SignalingPrefix = typeof XR_PREFIX | typeof XR_H_PREFIX;\nexport type SignalingRecipient = \"h\" | \"c\";\n\nexport const HANDSHAKE_TIMEOUT_MS = 30_000;\nexport const HANDSHAKE_RESEND_INTERVAL_MS = 2000;\n\nexport const composeFrame = (\n prefix: SignalingPrefix,\n recipient: SignalingRecipient,\n ciphertext: string,\n): string => prefix + recipient + ciphertext;\n\nexport const parseFrame = (\n frame: string,\n): { prefix: SignalingPrefix; recipient: SignalingRecipient; ciphertext: string; } | undefined => {\n const prefix = frame.slice(0, 1);\n\n if (prefix !== XR_PREFIX && prefix !== XR_H_PREFIX) return;\n\n const recipient = frame.slice(1, 2);\n\n if (recipient !== \"h\" && recipient !== \"c\") return;\n\n return { prefix, recipient, ciphertext: frame.slice(2) };\n};\n"]}
|
package/dist/handshake.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHandshakeCipher, generateKeyPair, hashPublicKey, } from "@openlv/core/encryption";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { createSignalingLayer,
|
|
3
|
+
import { createSignalingLayer, Status } from "./index.js";
|
|
4
|
+
const realSetTimeout = globalThis.setTimeout;
|
|
4
5
|
/**
|
|
5
6
|
* In-memory topic emulating a public relay: every publish is delivered to
|
|
6
7
|
* all subscribers (including the sender, like MQTT echo). A `drop` predicate
|
|
@@ -9,9 +10,14 @@ import { createSignalingLayer, SIGNAL_STATE } from "./index.js";
|
|
|
9
10
|
const neverDrop = () => false;
|
|
10
11
|
const createTopic = () => {
|
|
11
12
|
const subscribers = [];
|
|
13
|
+
const frames = [];
|
|
12
14
|
let drop = neverDrop;
|
|
13
|
-
let
|
|
15
|
+
let copies = 1;
|
|
14
16
|
return {
|
|
17
|
+
frames,
|
|
18
|
+
setCopies(count) {
|
|
19
|
+
copies = count;
|
|
20
|
+
},
|
|
15
21
|
setDrop(function_) {
|
|
16
22
|
drop = function_;
|
|
17
23
|
},
|
|
@@ -25,13 +31,16 @@ const createTopic = () => {
|
|
|
25
31
|
setup: () => { },
|
|
26
32
|
teardown: () => { },
|
|
27
33
|
publish: (payload) => {
|
|
28
|
-
const index =
|
|
34
|
+
const index = frames.length;
|
|
35
|
+
frames.push(payload);
|
|
29
36
|
if (drop(payload, index))
|
|
30
37
|
return;
|
|
31
38
|
// Deliver asynchronously, as a real relay would.
|
|
32
39
|
queueMicrotask(() => {
|
|
33
|
-
for (const subscriber of subscribers)
|
|
34
|
-
|
|
40
|
+
for (const subscriber of subscribers) {
|
|
41
|
+
for (let copy = 0; copy < copies; copy += 1)
|
|
42
|
+
subscriber(payload);
|
|
43
|
+
}
|
|
35
44
|
});
|
|
36
45
|
},
|
|
37
46
|
subscribe: (handler) => {
|
|
@@ -46,62 +55,39 @@ const CLIENT_CAPABILITIES = {
|
|
|
46
55
|
info: { identity: "com.example.wallet", name: "Example Wallet" },
|
|
47
56
|
};
|
|
48
57
|
const createPeer = async (channel, { isHost, h, k }) => {
|
|
49
|
-
const { encryptionKey, decryptionKey } = await generateKeyPair();
|
|
50
|
-
let relyingKey;
|
|
51
58
|
let peerCapabilities;
|
|
52
59
|
const layer = await createSignalingLayer(channel)({
|
|
53
60
|
isHost,
|
|
54
61
|
h,
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
canEncrypt: () => relyingKey !== undefined,
|
|
58
|
-
encrypt: (message) => {
|
|
59
|
-
if (!relyingKey)
|
|
60
|
-
throw new Error("no peer key");
|
|
61
|
-
return relyingKey.encrypt(message);
|
|
62
|
-
},
|
|
63
|
-
decrypt: (message) => decryptionKey.decrypt(message),
|
|
64
|
-
rpDiscovered: async (rpKey) => {
|
|
65
|
-
relyingKey = await parseEncryptionKey(rpKey);
|
|
66
|
-
},
|
|
62
|
+
handshakeCipher: await createHandshakeCipher(k),
|
|
63
|
+
keyPair: generateKeyPair(),
|
|
67
64
|
capabilities: CLIENT_CAPABILITIES,
|
|
68
|
-
peerCapabilities: (capabilities) => {
|
|
69
|
-
peerCapabilities = capabilities;
|
|
70
|
-
},
|
|
71
65
|
});
|
|
72
|
-
|
|
66
|
+
layer.peerCapabilities.subscribe(async (capabilities) => {
|
|
67
|
+
peerCapabilities = capabilities;
|
|
68
|
+
});
|
|
69
|
+
return { layer, getPeerCapabilities: () => peerCapabilities };
|
|
73
70
|
};
|
|
74
71
|
const K = "00112233445566778899aabbccddeeff";
|
|
75
72
|
const setupPair = async (topic) => {
|
|
76
73
|
// Mirror createSession: the host's h is the hash of its own key, the
|
|
77
74
|
// client receives that h out-of-band (via the URI).
|
|
78
|
-
const hostKeys =
|
|
79
|
-
const h = await hashPublicKey(hostKeys.
|
|
75
|
+
const hostKeys = generateKeyPair();
|
|
76
|
+
const h = await hashPublicKey(hostKeys.publicKey);
|
|
80
77
|
const HOST_CAPABILITIES = {
|
|
81
78
|
transports: ["wrtc", "ws"],
|
|
82
79
|
info: { identity: "com.example.dapp", name: "Example dApp" },
|
|
83
80
|
};
|
|
84
|
-
let hostRelying;
|
|
85
81
|
let hostPeerCapabilities;
|
|
86
82
|
const host = await createSignalingLayer(topic.channel())({
|
|
87
83
|
isHost: true,
|
|
88
84
|
h,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
canEncrypt: () => hostRelying !== undefined,
|
|
92
|
-
encrypt: (message) => {
|
|
93
|
-
if (!hostRelying)
|
|
94
|
-
throw new Error("no peer key");
|
|
95
|
-
return hostRelying.encrypt(message);
|
|
96
|
-
},
|
|
97
|
-
decrypt: (message) => hostKeys.decryptionKey.decrypt(message),
|
|
98
|
-
rpDiscovered: async (rpKey) => {
|
|
99
|
-
hostRelying = await parseEncryptionKey(rpKey);
|
|
100
|
-
},
|
|
85
|
+
handshakeCipher: await createHandshakeCipher(K),
|
|
86
|
+
keyPair: hostKeys,
|
|
101
87
|
capabilities: HOST_CAPABILITIES,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
88
|
+
});
|
|
89
|
+
host.peerCapabilities.subscribe((capabilities) => {
|
|
90
|
+
hostPeerCapabilities = capabilities;
|
|
105
91
|
});
|
|
106
92
|
const { layer: client, getPeerCapabilities: getClientPeerCapabilities } = await createPeer(topic.channel(), {
|
|
107
93
|
isHost: false,
|
|
@@ -117,16 +103,37 @@ const setupPair = async (topic) => {
|
|
|
117
103
|
};
|
|
118
104
|
};
|
|
119
105
|
const waitForState = (layer, target) => new Promise((resolve, reject) => {
|
|
120
|
-
if (layer.
|
|
106
|
+
if (layer.status.get() === target)
|
|
121
107
|
return resolve();
|
|
122
|
-
layer.
|
|
108
|
+
layer.status.subscribe((state) => {
|
|
123
109
|
if (state === target)
|
|
124
110
|
resolve();
|
|
125
|
-
if (state ===
|
|
111
|
+
if (state === Status.ERROR && target !== Status.ERROR) {
|
|
112
|
+
// eslint-disable-next-line unicorn/no-multiple-promise-resolver-calls
|
|
126
113
|
reject(new Error("signaling errored"));
|
|
127
114
|
}
|
|
128
115
|
});
|
|
129
116
|
});
|
|
117
|
+
const bothEncrypted = (first, second) => Promise.all([
|
|
118
|
+
waitForState(first, Status.ENCRYPTED),
|
|
119
|
+
waitForState(second, Status.ENCRYPTED),
|
|
120
|
+
]);
|
|
121
|
+
// The crypto behind each send finishes in real time, outside the fake clock
|
|
122
|
+
const advance = async (milliseconds) => {
|
|
123
|
+
await vi.advanceTimersByTimeAsync(milliseconds);
|
|
124
|
+
await new Promise(resolve => realSetTimeout(resolve, 50));
|
|
125
|
+
};
|
|
126
|
+
const recordHistory = (layer) => {
|
|
127
|
+
const statuses = [];
|
|
128
|
+
const peers = [];
|
|
129
|
+
layer.status.subscribe((value) => {
|
|
130
|
+
statuses.push(value);
|
|
131
|
+
});
|
|
132
|
+
layer.peer.subscribe((value) => {
|
|
133
|
+
peers.push(value);
|
|
134
|
+
});
|
|
135
|
+
return { statuses, peers };
|
|
136
|
+
};
|
|
130
137
|
describe("signaling handshake (in-memory relay)", () => {
|
|
131
138
|
let host;
|
|
132
139
|
let client;
|
|
@@ -140,8 +147,8 @@ describe("signaling handshake (in-memory relay)", () => {
|
|
|
140
147
|
({ host, client } = pair);
|
|
141
148
|
await host.setup();
|
|
142
149
|
const encrypted = Promise.all([
|
|
143
|
-
waitForState(host,
|
|
144
|
-
waitForState(client,
|
|
150
|
+
waitForState(host, Status.ENCRYPTED),
|
|
151
|
+
waitForState(client, Status.ENCRYPTED),
|
|
145
152
|
]);
|
|
146
153
|
await client.setup();
|
|
147
154
|
await encrypted;
|
|
@@ -155,8 +162,8 @@ describe("signaling handshake (in-memory relay)", () => {
|
|
|
155
162
|
({ host, client } = pair);
|
|
156
163
|
await host.setup();
|
|
157
164
|
const encrypted = Promise.all([
|
|
158
|
-
waitForState(host,
|
|
159
|
-
waitForState(client,
|
|
165
|
+
waitForState(host, Status.ENCRYPTED),
|
|
166
|
+
waitForState(client, Status.ENCRYPTED),
|
|
160
167
|
]);
|
|
161
168
|
await client.setup();
|
|
162
169
|
await encrypted;
|
|
@@ -174,52 +181,182 @@ describe("signaling handshake (in-memory relay)", () => {
|
|
|
174
181
|
topic.inject("hcQUJDRA==");
|
|
175
182
|
topic.inject("xh!!!not-base64!!!");
|
|
176
183
|
const encrypted = Promise.all([
|
|
177
|
-
waitForState(host,
|
|
178
|
-
waitForState(client,
|
|
184
|
+
waitForState(host, Status.ENCRYPTED),
|
|
185
|
+
waitForState(client, Status.ENCRYPTED),
|
|
179
186
|
]);
|
|
180
187
|
await client.setup();
|
|
181
188
|
topic.inject("hcgarbage-mid-handshake");
|
|
182
189
|
await encrypted;
|
|
183
190
|
});
|
|
184
|
-
describe("
|
|
191
|
+
describe("timeout", () => {
|
|
192
|
+
beforeEach(() => {
|
|
193
|
+
vi.useFakeTimers();
|
|
194
|
+
});
|
|
195
|
+
afterEach(() => {
|
|
196
|
+
vi.useRealTimers();
|
|
197
|
+
});
|
|
198
|
+
it("fails a joining peer whose host does not answer after 30 seconds", async () => {
|
|
199
|
+
const topic = createTopic();
|
|
200
|
+
topic.setDrop(() => true);
|
|
201
|
+
({ host, client } = await setupPair(topic));
|
|
202
|
+
await host.setup();
|
|
203
|
+
await client.setup();
|
|
204
|
+
await vi.advanceTimersByTimeAsync(29999);
|
|
205
|
+
expect(client.status.get()).toBe(Status.HANDSHAKE);
|
|
206
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
207
|
+
expect(client.status.get()).toBe(Status.ERROR);
|
|
208
|
+
});
|
|
209
|
+
it("fails a host 30 seconds after a flash when the joining peer goes quiet", async () => {
|
|
210
|
+
const topic = createTopic();
|
|
211
|
+
topic.setDrop(payload => !payload.startsWith("hh"));
|
|
212
|
+
({ host, client } = await setupPair(topic));
|
|
213
|
+
await host.setup();
|
|
214
|
+
await client.setup();
|
|
215
|
+
await waitForState(host, Status.HANDSHAKE);
|
|
216
|
+
await vi.advanceTimersByTimeAsync(29999);
|
|
217
|
+
expect(host.status.get()).toBe(Status.HANDSHAKE);
|
|
218
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
219
|
+
expect(host.status.get()).toBe(Status.ERROR);
|
|
220
|
+
});
|
|
221
|
+
it("keeps a completed handshake encrypted past the limit", async () => {
|
|
222
|
+
const topic = createTopic();
|
|
223
|
+
({ host, client } = await setupPair(topic));
|
|
224
|
+
await host.setup();
|
|
225
|
+
const encrypted = Promise.all([
|
|
226
|
+
waitForState(host, Status.ENCRYPTED),
|
|
227
|
+
waitForState(client, Status.ENCRYPTED),
|
|
228
|
+
]);
|
|
229
|
+
await client.setup();
|
|
230
|
+
await encrypted;
|
|
231
|
+
await vi.advanceTimersByTimeAsync(60000);
|
|
232
|
+
expect(host.status.get()).toBe(Status.ENCRYPTED);
|
|
233
|
+
expect(client.status.get()).toBe(Status.ENCRYPTED);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
it("ignores duplicate deliveries of every frame", async () => {
|
|
237
|
+
const topic = createTopic();
|
|
238
|
+
topic.setCopies(2);
|
|
239
|
+
({ host, client } = await setupPair(topic));
|
|
240
|
+
const hostHistory = recordHistory(host);
|
|
241
|
+
const clientHistory = recordHistory(client);
|
|
242
|
+
await host.setup();
|
|
243
|
+
const encrypted = bothEncrypted(host, client);
|
|
244
|
+
await client.setup();
|
|
245
|
+
await encrypted;
|
|
246
|
+
const path = ["standby", "connecting", "ready", "handshake", "handshake-partial", "encrypted"];
|
|
247
|
+
expect(hostHistory.statuses).toEqual(path);
|
|
248
|
+
expect(clientHistory.statuses).toEqual(path);
|
|
249
|
+
// The initial value and the one recorded peer
|
|
250
|
+
expect(hostHistory.peers).toHaveLength(2);
|
|
251
|
+
expect(clientHistory.peers).toHaveLength(2);
|
|
252
|
+
});
|
|
253
|
+
it("answers each duplicate capabilities of the host after the handshake", async () => {
|
|
254
|
+
const topic = createTopic();
|
|
255
|
+
({ host, client } = await setupPair(topic));
|
|
256
|
+
await host.setup();
|
|
257
|
+
const encrypted = bothEncrypted(host, client);
|
|
258
|
+
await client.setup();
|
|
259
|
+
await encrypted;
|
|
260
|
+
const sent = topic.frames.length;
|
|
261
|
+
// "xc" is a peer-key frame for the joining peer: only the host's capabilities
|
|
262
|
+
const hostCapabilities = topic.frames.filter(frame => frame.startsWith("xc"));
|
|
263
|
+
for (const frame of [...hostCapabilities, ...hostCapabilities])
|
|
264
|
+
topic.inject(frame);
|
|
265
|
+
await vi.waitFor(() => expect(topic.frames).toHaveLength(sent + 2));
|
|
266
|
+
expect(topic.frames.slice(sent).every(frame => frame.startsWith("xh"))).toBe(true);
|
|
267
|
+
expect(host.status.get()).toBe(Status.ENCRYPTED);
|
|
268
|
+
expect(client.status.get()).toBe(Status.ENCRYPTED);
|
|
269
|
+
});
|
|
270
|
+
describe("re-send", () => {
|
|
185
271
|
beforeEach(() => {
|
|
186
272
|
vi.useFakeTimers();
|
|
187
273
|
});
|
|
188
274
|
afterEach(() => {
|
|
189
275
|
vi.useRealTimers();
|
|
190
276
|
});
|
|
191
|
-
it("
|
|
277
|
+
it("sends a dropped flash again after 2 seconds", async () => {
|
|
278
|
+
const topic = createTopic();
|
|
279
|
+
topic.setDrop((_payload, index) => index === 0);
|
|
280
|
+
({ host, client } = await setupPair(topic));
|
|
281
|
+
await host.setup();
|
|
282
|
+
await client.setup();
|
|
283
|
+
await advance(1999);
|
|
284
|
+
expect(host.status.get()).toBe(Status.READY);
|
|
285
|
+
await advance(1);
|
|
286
|
+
await bothEncrypted(host, client);
|
|
287
|
+
});
|
|
288
|
+
it("recovers when the first delivery of every frame is dropped", async () => {
|
|
192
289
|
const topic = createTopic();
|
|
193
|
-
// Drop the first delivery of every frame; re-sends get through.
|
|
194
290
|
const seen = new Set();
|
|
195
291
|
topic.setDrop((payload) => {
|
|
196
|
-
|
|
292
|
+
const key = payload.slice(0, 2) + payload.length;
|
|
293
|
+
if (seen.has(key))
|
|
197
294
|
return false;
|
|
198
|
-
seen.add(
|
|
295
|
+
seen.add(key);
|
|
199
296
|
return true;
|
|
200
297
|
});
|
|
201
298
|
({ host, client } = await setupPair(topic));
|
|
202
299
|
await host.setup();
|
|
203
300
|
await client.setup();
|
|
204
|
-
const encrypted =
|
|
205
|
-
waitForState(host, SIGNAL_STATE.ENCRYPTED),
|
|
206
|
-
waitForState(client, SIGNAL_STATE.ENCRYPTED),
|
|
207
|
-
]);
|
|
208
|
-
// Let several resend intervals elapse.
|
|
301
|
+
const encrypted = bothEncrypted(host, client);
|
|
209
302
|
for (let index = 0; index < 10; index += 1) {
|
|
210
|
-
await
|
|
303
|
+
await advance(2100);
|
|
211
304
|
}
|
|
212
305
|
await encrypted;
|
|
213
306
|
});
|
|
214
|
-
it("
|
|
307
|
+
it("stops re-sending once encrypted", async () => {
|
|
308
|
+
const topic = createTopic();
|
|
309
|
+
({ host, client } = await setupPair(topic));
|
|
310
|
+
await host.setup();
|
|
311
|
+
const encrypted = bothEncrypted(host, client);
|
|
312
|
+
await client.setup();
|
|
313
|
+
await encrypted;
|
|
314
|
+
const sent = topic.frames.length;
|
|
315
|
+
await advance(60000);
|
|
316
|
+
expect(topic.frames).toHaveLength(sent);
|
|
317
|
+
});
|
|
318
|
+
it("re-sends every 2 seconds until the limit, then stops", async () => {
|
|
215
319
|
const topic = createTopic();
|
|
216
320
|
topic.setDrop(() => true);
|
|
217
321
|
({ host, client } = await setupPair(topic));
|
|
322
|
+
await host.setup();
|
|
323
|
+
await client.setup();
|
|
324
|
+
await advance(29999);
|
|
325
|
+
// The first flash and one re-send every 2 seconds from 2 s to 28 s
|
|
326
|
+
expect(topic.frames).toHaveLength(15);
|
|
327
|
+
await advance(1);
|
|
328
|
+
expect(client.status.get()).toBe(Status.ERROR);
|
|
329
|
+
await advance(60000);
|
|
330
|
+
expect(topic.frames).toHaveLength(15);
|
|
331
|
+
});
|
|
332
|
+
it("stops re-sending on teardown", async () => {
|
|
333
|
+
const topic = createTopic();
|
|
334
|
+
topic.setDrop(() => true);
|
|
335
|
+
({ host, client } = await setupPair(topic));
|
|
336
|
+
await host.setup();
|
|
337
|
+
await client.setup();
|
|
338
|
+
await advance(5000);
|
|
339
|
+
await client.teardown();
|
|
340
|
+
const sent = topic.frames.length;
|
|
341
|
+
await advance(20000);
|
|
342
|
+
expect(topic.frames).toHaveLength(sent);
|
|
343
|
+
});
|
|
344
|
+
it("stops re-sending a step once the next one starts", async () => {
|
|
345
|
+
const topic = createTopic();
|
|
346
|
+
// The host's capabilities never arrive, so both peers stay in handshake-partial
|
|
347
|
+
topic.setDrop(payload => payload.startsWith("xc"));
|
|
348
|
+
({ host, client } = await setupPair(topic));
|
|
349
|
+
await host.setup();
|
|
218
350
|
await client.setup();
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
351
|
+
await Promise.all([host, client].map(layer => waitForState(layer, Status.HANDSHAKE_PARTIAL)));
|
|
352
|
+
const sent = topic.frames.length;
|
|
353
|
+
const sentSince = (prefix) => topic.frames.slice(sent).filter(frame => frame.startsWith(prefix));
|
|
354
|
+
await advance(10000);
|
|
355
|
+
// The flash and the host's pubkey stopped; the steps of handshake-partial still repeat
|
|
356
|
+
expect(sentSince("hh")).toEqual([]);
|
|
357
|
+
expect(sentSince("hc")).toEqual([]);
|
|
358
|
+
expect(sentSince("xh").length).toBeGreaterThan(1);
|
|
359
|
+
expect(sentSince("xc").length).toBeGreaterThan(1);
|
|
223
360
|
});
|
|
224
361
|
});
|
|
225
362
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../src/handshake.test.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n deriveSymmetricKey,\n type EncryptionKey,\n generateKeyPair,\n hashPublicKey,\n parseEncryptionKey,\n} from \"@openlv/core/encryption\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { createSignalingLayer, SIGNAL_STATE, type SignalingLayer } from \"./index.js\";\nimport type { PeerCapabilities } from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\n\n/**\n * In-memory topic emulating a public relay: every publish is delivered to\n * all subscribers (including the sender, like MQTT echo). A `drop` predicate\n * simulates lossy delivery.\n */\nconst neverDrop = () => false;\n\nconst createTopic = () => {\n const subscribers: ((payload: string) => void)[] = [];\n let drop: (payload: string, index: number) => boolean = neverDrop;\n let published = 0;\n\n return {\n setDrop(function_: (payload: string, index: number) => boolean) {\n drop = function_;\n },\n inject(payload: string) {\n for (const subscriber of subscribers) subscriber(payload);\n },\n channel(): SignalingChannel {\n return {\n type: \"memory\",\n setup: () => {},\n teardown: () => {},\n publish: (payload: string) => {\n const index = published++;\n\n if (drop(payload, index)) return;\n\n // Deliver asynchronously, as a real relay would.\n queueMicrotask(() => {\n for (const subscriber of subscribers) subscriber(payload);\n });\n },\n subscribe: (handler) => {\n subscribers.push(handler);\n },\n };\n },\n };\n};\n\nconst CLIENT_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\"],\n info: { identity: \"com.example.wallet\", name: \"Example Wallet\" },\n};\n\nconst createPeer = async (\n channel: SignalingChannel,\n { isHost, h, k }: { isHost: boolean; h: string; k: string; },\n) => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n let relyingKey: EncryptionKey | undefined;\n let peerCapabilities: PeerCapabilities | undefined;\n\n const layer = await createSignalingLayer(channel)({\n isHost,\n h,\n k: await deriveSymmetricKey(k),\n publicKey: encryptionKey,\n canEncrypt: () => relyingKey !== undefined,\n encrypt: (message: string) => {\n if (!relyingKey) throw new Error(\"no peer key\");\n\n return relyingKey.encrypt(message);\n },\n decrypt: (message: string) => decryptionKey.decrypt(message),\n rpDiscovered: async (rpKey: string) => {\n relyingKey = await parseEncryptionKey(rpKey);\n },\n capabilities: CLIENT_CAPABILITIES,\n peerCapabilities: (capabilities) => {\n peerCapabilities = capabilities;\n },\n });\n\n return { layer, encryptionKey, getPeerCapabilities: () => peerCapabilities };\n};\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst setupPair = async (topic: ReturnType<typeof createTopic>) => {\n // Mirror createSession: the host's h is the hash of its own key, the\n // client receives that h out-of-band (via the URI).\n const hostKeys = await generateKeyPair();\n const h = await hashPublicKey(hostKeys.encryptionKey);\n\n const HOST_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\", \"ws\"],\n info: { identity: \"com.example.dapp\", name: \"Example dApp\" },\n };\n\n let hostRelying: EncryptionKey | undefined;\n let hostPeerCapabilities: PeerCapabilities | undefined;\n const host = await createSignalingLayer(topic.channel())({\n isHost: true,\n h,\n k: await deriveSymmetricKey(K),\n publicKey: hostKeys.encryptionKey,\n canEncrypt: () => hostRelying !== undefined,\n encrypt: (message: string) => {\n if (!hostRelying) throw new Error(\"no peer key\");\n\n return hostRelying.encrypt(message);\n },\n decrypt: (message: string) => hostKeys.decryptionKey.decrypt(message),\n rpDiscovered: async (rpKey: string) => {\n hostRelying = await parseEncryptionKey(rpKey);\n },\n capabilities: HOST_CAPABILITIES,\n peerCapabilities: (capabilities) => {\n hostPeerCapabilities = capabilities;\n },\n });\n\n const { layer: client, getPeerCapabilities: getClientPeerCapabilities }\n = await createPeer(topic.channel(), {\n isHost: false,\n h,\n k: K,\n });\n\n return {\n host,\n client,\n hostCapabilities: HOST_CAPABILITIES,\n getHostPeerCapabilities: () => hostPeerCapabilities,\n getClientPeerCapabilities,\n };\n};\n\nconst waitForState = (layer: SignalingLayer, target: string) =>\n new Promise<void>((resolve, reject) => {\n if (layer.getState().state === target) return resolve();\n\n layer.on(\"state_change\", (state) => {\n if (state === target) resolve();\n\n if (state === SIGNAL_STATE.ERROR && target !== SIGNAL_STATE.ERROR) {\n reject(new Error(\"signaling errored\"));\n }\n });\n });\n\ndescribe(\"signaling handshake (in-memory relay)\", () => {\n let host: SignalingLayer;\n let client: SignalingLayer;\n\n afterEach(async () => {\n await host?.teardown();\n await client?.teardown();\n });\n\n it(\"completes and exchanges data on a reliable relay\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n const received = new Promise<object>(resolve => host.on(\"message\", resolve));\n\n await client.send({ hello: \"world\" });\n expect(await received).toEqual({ hello: \"world\" });\n });\n\n it(\"exchanges capabilities in both directions during the handshake\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n expect(pair.getClientPeerCapabilities()).toEqual(pair.hostCapabilities);\n expect(pair.getHostPeerCapabilities()).toEqual(CLIENT_CAPABILITIES);\n });\n\n it(\"ignores garbage frames on the public topic\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n // Garbage before and during the handshake: random text, valid-looking\n // frames with undecryptable bodies, and truncated frames.\n topic.inject(\"not-a-frame\");\n topic.inject(\"hh\");\n topic.inject(\"hcQUJDRA==\");\n topic.inject(\"xh!!!not-base64!!!\");\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n topic.inject(\"hcgarbage-mid-handshake\");\n await encrypted;\n });\n\n describe(\"lossy relay\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"recovers when handshake frames are dropped\", async () => {\n const topic = createTopic();\n\n // Drop the first delivery of every frame; re-sends get through.\n const seen = new Set<string>();\n\n topic.setDrop((payload) => {\n if (seen.has(payload.slice(0, 2) + payload.length)) return false;\n\n seen.add(payload.slice(0, 2) + payload.length);\n\n return true;\n });\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n // Let several resend intervals elapse.\n for (let index = 0; index < 10; index += 1) {\n await vi.advanceTimersByTimeAsync(2100);\n }\n\n await encrypted;\n });\n\n it(\"errors out when the peer never appears\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n\n ({ host, client } = await setupPair(topic));\n\n await client.setup();\n\n const errored = waitForState(client, SIGNAL_STATE.ERROR);\n\n await vi.advanceTimersByTimeAsync(31_000);\n await errored;\n expect(client.getState().state).toBe(SIGNAL_STATE.ERROR);\n });\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../src/handshake.test.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createHandshakeCipher,\n generateKeyPair,\n hashPublicKey,\n} from \"@openlv/core/encryption\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { createSignalingLayer, Status, type SignalingLayer } from \"./index.js\";\nimport type { PeerCapabilities } from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\n\nconst realSetTimeout = globalThis.setTimeout;\n\n/**\n * In-memory topic emulating a public relay: every publish is delivered to\n * all subscribers (including the sender, like MQTT echo). A `drop` predicate\n * simulates lossy delivery.\n */\nconst neverDrop = () => false;\n\ntype Topic = {\n setDrop: (function_: (payload: string, index: number) => boolean) => void;\n setCopies: (copies: number) => void;\n inject: (payload: string) => void;\n channel: () => SignalingChannel;\n // Every published payload, including the dropped ones\n frames: string[];\n};\n\nconst createTopic = (): Topic => {\n const subscribers: ((payload: string) => void)[] = [];\n const frames: string[] = [];\n let drop: (payload: string, index: number) => boolean = neverDrop;\n let copies = 1;\n\n return {\n frames,\n setCopies(count: number) {\n copies = count;\n },\n setDrop(function_: (payload: string, index: number) => boolean) {\n drop = function_;\n },\n inject(payload: string) {\n for (const subscriber of subscribers) subscriber(payload);\n },\n channel(): SignalingChannel {\n return {\n type: \"memory\",\n setup: () => {},\n teardown: () => {},\n publish: (payload: string) => {\n const index = frames.length;\n\n frames.push(payload);\n\n if (drop(payload, index)) return;\n\n // Deliver asynchronously, as a real relay would.\n queueMicrotask(() => {\n for (const subscriber of subscribers) {\n for (let copy = 0; copy < copies; copy += 1) subscriber(payload);\n }\n });\n },\n subscribe: (handler) => {\n subscribers.push(handler);\n },\n };\n },\n };\n};\n\nconst CLIENT_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\"],\n info: { identity: \"com.example.wallet\", name: \"Example Wallet\" },\n};\n\nconst createPeer = async (\n channel: SignalingChannel,\n { isHost, h, k }: { isHost: boolean; h: string; k: string; },\n) => {\n let peerCapabilities: PeerCapabilities | undefined;\n\n const layer = await createSignalingLayer(channel)({\n isHost,\n h,\n handshakeCipher: await createHandshakeCipher(k),\n keyPair: generateKeyPair(),\n capabilities: CLIENT_CAPABILITIES,\n });\n\n layer.peerCapabilities.subscribe(async (capabilities) => {\n peerCapabilities = capabilities;\n });\n\n return { layer, getPeerCapabilities: () => peerCapabilities };\n};\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst setupPair = async (topic: ReturnType<typeof createTopic>) => {\n // Mirror createSession: the host's h is the hash of its own key, the\n // client receives that h out-of-band (via the URI).\n const hostKeys = generateKeyPair();\n const h = await hashPublicKey(hostKeys.publicKey);\n\n const HOST_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\", \"ws\"],\n info: { identity: \"com.example.dapp\", name: \"Example dApp\" },\n };\n\n let hostPeerCapabilities: PeerCapabilities | undefined;\n const host = await createSignalingLayer(topic.channel())({\n isHost: true,\n h,\n handshakeCipher: await createHandshakeCipher(K),\n keyPair: hostKeys,\n capabilities: HOST_CAPABILITIES,\n });\n\n host.peerCapabilities.subscribe((capabilities) => {\n hostPeerCapabilities = capabilities;\n });\n\n const { layer: client, getPeerCapabilities: getClientPeerCapabilities }\n = await createPeer(topic.channel(), {\n isHost: false,\n h,\n k: K,\n });\n\n return {\n host,\n client,\n hostCapabilities: HOST_CAPABILITIES,\n getHostPeerCapabilities: () => hostPeerCapabilities,\n getClientPeerCapabilities,\n };\n};\n\nconst waitForState = (layer: SignalingLayer, target: string) =>\n new Promise<void>((resolve, reject) => {\n if (layer.status.get() === target) return resolve();\n\n layer.status.subscribe((state) => {\n if (state === target) resolve();\n\n if (state === Status.ERROR && target !== Status.ERROR) {\n // eslint-disable-next-line unicorn/no-multiple-promise-resolver-calls\n reject(new Error(\"signaling errored\"));\n }\n });\n });\n\nconst bothEncrypted = (first: SignalingLayer, second: SignalingLayer) => Promise.all([\n waitForState(first, Status.ENCRYPTED),\n waitForState(second, Status.ENCRYPTED),\n]);\n\n// The crypto behind each send finishes in real time, outside the fake clock\nconst advance = async (milliseconds: number) => {\n await vi.advanceTimersByTimeAsync(milliseconds);\n await new Promise(resolve => realSetTimeout(resolve, 50));\n};\n\nconst recordHistory = (layer: SignalingLayer) => {\n const statuses: string[] = [];\n const peers: unknown[] = [];\n\n layer.status.subscribe((value) => {\n statuses.push(value);\n });\n layer.peer.subscribe((value) => {\n peers.push(value);\n });\n\n return { statuses, peers };\n};\n\ndescribe(\"signaling handshake (in-memory relay)\", () => {\n let host: SignalingLayer;\n let client: SignalingLayer;\n\n afterEach(async () => {\n await host?.teardown();\n await client?.teardown();\n });\n\n it(\"completes and exchanges data on a reliable relay\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n const received = new Promise<object>(resolve => host.on(\"message\", resolve));\n\n await client.send({ hello: \"world\" });\n expect(await received).toEqual({ hello: \"world\" });\n });\n\n it(\"exchanges capabilities in both directions during the handshake\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n expect(pair.getClientPeerCapabilities()).toEqual(pair.hostCapabilities);\n expect(pair.getHostPeerCapabilities()).toEqual(CLIENT_CAPABILITIES);\n });\n\n it(\"ignores garbage frames on the public topic\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n // Garbage before and during the handshake: random text, valid-looking\n // frames with undecryptable bodies, and truncated frames.\n topic.inject(\"not-a-frame\");\n topic.inject(\"hh\");\n topic.inject(\"hcQUJDRA==\");\n topic.inject(\"xh!!!not-base64!!!\");\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n topic.inject(\"hcgarbage-mid-handshake\");\n await encrypted;\n });\n\n describe(\"timeout\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"fails a joining peer whose host does not answer after 30 seconds\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await vi.advanceTimersByTimeAsync(29_999);\n expect(client.status.get()).toBe(Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(1);\n expect(client.status.get()).toBe(Status.ERROR);\n });\n\n it(\"fails a host 30 seconds after a flash when the joining peer goes quiet\", async () => {\n const topic = createTopic();\n\n topic.setDrop(payload => !payload.startsWith(\"hh\"));\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await waitForState(host, Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(29_999);\n expect(host.status.get()).toBe(Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(1);\n expect(host.status.get()).toBe(Status.ERROR);\n });\n\n it(\"keeps a completed handshake encrypted past the limit\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n await vi.advanceTimersByTimeAsync(60_000);\n expect(host.status.get()).toBe(Status.ENCRYPTED);\n expect(client.status.get()).toBe(Status.ENCRYPTED);\n });\n });\n\n it(\"ignores duplicate deliveries of every frame\", async () => {\n const topic = createTopic();\n\n topic.setCopies(2);\n ({ host, client } = await setupPair(topic));\n\n const hostHistory = recordHistory(host);\n const clientHistory = recordHistory(client);\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const path = [\"standby\", \"connecting\", \"ready\", \"handshake\", \"handshake-partial\", \"encrypted\"];\n\n expect(hostHistory.statuses).toEqual(path);\n expect(clientHistory.statuses).toEqual(path);\n // The initial value and the one recorded peer\n expect(hostHistory.peers).toHaveLength(2);\n expect(clientHistory.peers).toHaveLength(2);\n });\n\n it(\"answers each duplicate capabilities of the host after the handshake\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const sent = topic.frames.length;\n // \"xc\" is a peer-key frame for the joining peer: only the host's capabilities\n const hostCapabilities = topic.frames.filter(frame => frame.startsWith(\"xc\"));\n\n for (const frame of [...hostCapabilities, ...hostCapabilities]) topic.inject(frame);\n\n await vi.waitFor(() => expect(topic.frames).toHaveLength(sent + 2));\n\n expect(topic.frames.slice(sent).every(frame => frame.startsWith(\"xh\"))).toBe(true);\n expect(host.status.get()).toBe(Status.ENCRYPTED);\n expect(client.status.get()).toBe(Status.ENCRYPTED);\n });\n\n describe(\"re-send\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"sends a dropped flash again after 2 seconds\", async () => {\n const topic = createTopic();\n\n topic.setDrop((_payload, index) => index === 0);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await advance(1999);\n expect(host.status.get()).toBe(Status.READY);\n\n await advance(1);\n await bothEncrypted(host, client);\n });\n\n it(\"recovers when the first delivery of every frame is dropped\", async () => {\n const topic = createTopic();\n const seen = new Set<string>();\n\n topic.setDrop((payload) => {\n const key = payload.slice(0, 2) + payload.length;\n\n if (seen.has(key)) return false;\n\n seen.add(key);\n\n return true;\n });\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n for (let index = 0; index < 10; index += 1) {\n await advance(2100);\n }\n\n await encrypted;\n });\n\n it(\"stops re-sending once encrypted\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const sent = topic.frames.length;\n\n await advance(60_000);\n expect(topic.frames).toHaveLength(sent);\n });\n\n it(\"re-sends every 2 seconds until the limit, then stops\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await advance(29_999);\n // The first flash and one re-send every 2 seconds from 2 s to 28 s\n expect(topic.frames).toHaveLength(15);\n\n await advance(1);\n expect(client.status.get()).toBe(Status.ERROR);\n\n await advance(60_000);\n expect(topic.frames).toHaveLength(15);\n });\n\n it(\"stops re-sending on teardown\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await advance(5000);\n await client.teardown();\n\n const sent = topic.frames.length;\n\n await advance(20_000);\n expect(topic.frames).toHaveLength(sent);\n });\n\n it(\"stops re-sending a step once the next one starts\", async () => {\n const topic = createTopic();\n\n // The host's capabilities never arrive, so both peers stay in handshake-partial\n topic.setDrop(payload => payload.startsWith(\"xc\"));\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await Promise.all([host, client].map(layer => waitForState(layer, Status.HANDSHAKE_PARTIAL)));\n\n const sent = topic.frames.length;\n const sentSince = (prefix: string) => topic.frames.slice(sent).filter(frame => frame.startsWith(prefix));\n\n await advance(10_000);\n\n // The flash and the host's pubkey stopped; the steps of handshake-partial still repeat\n expect(sentSince(\"hh\")).toEqual([]);\n expect(sentSince(\"hc\")).toEqual([]);\n expect(sentSince(\"xh\").length).toBeGreaterThan(1);\n expect(sentSince(\"xc\").length).toBeGreaterThan(1);\n });\n });\n});\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type Observable } from "@openlv/core";
|
|
2
|
+
import type { Cipher, KeyPair, PublicKey } from "@openlv/core/encryption";
|
|
2
3
|
import { EventEmitter } from "eventemitter3";
|
|
3
|
-
import type { MaybePromise } from "viem";
|
|
4
4
|
import { type PeerCapabilities } from "./messages.js";
|
|
5
5
|
import type { SignalingChannel } from "./protocol.js";
|
|
6
6
|
export * from "./messages.js";
|
|
7
7
|
export * from "./protocol.js";
|
|
8
|
-
export declare const
|
|
8
|
+
export declare const Status: {
|
|
9
9
|
readonly STANDBY: "standby";
|
|
10
10
|
readonly CONNECTING: "connecting";
|
|
11
11
|
readonly READY: "ready";
|
|
@@ -14,39 +14,36 @@ export declare const SIGNAL_STATE: {
|
|
|
14
14
|
readonly ENCRYPTED: "encrypted";
|
|
15
15
|
readonly ERROR: "error";
|
|
16
16
|
};
|
|
17
|
-
export type
|
|
17
|
+
export type Status = (typeof Status)[keyof typeof Status];
|
|
18
18
|
export type SignalEventMap = {
|
|
19
|
-
state_change: (state: SignalState) => void;
|
|
20
19
|
message: (message: object) => void;
|
|
21
20
|
};
|
|
22
|
-
export type
|
|
21
|
+
export type SignalingHooks = {
|
|
23
22
|
isHost: boolean;
|
|
24
23
|
h: string;
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
handshakeCipher: Cipher;
|
|
25
|
+
keyPair: KeyPair;
|
|
27
26
|
capabilities: PeerCapabilities;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
canEncrypt: () => boolean;
|
|
27
|
+
};
|
|
28
|
+
export type Peer = {
|
|
29
|
+
peerKey: PublicKey;
|
|
30
|
+
cipher: Cipher;
|
|
33
31
|
};
|
|
34
32
|
export type SignalingContext = {
|
|
35
33
|
type: string;
|
|
36
|
-
send: (message: object) =>
|
|
37
|
-
setup: () =>
|
|
38
|
-
teardown: () =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
34
|
+
send: (message: object) => Promise<void>;
|
|
35
|
+
setup: () => Promise<void>;
|
|
36
|
+
teardown: () => Promise<void>;
|
|
37
|
+
status: Observable<Status>;
|
|
38
|
+
peer: Observable<Peer | undefined>;
|
|
39
|
+
peerCapabilities: Observable<PeerCapabilities | undefined>;
|
|
42
40
|
};
|
|
43
41
|
export type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;
|
|
44
|
-
export type SignalingLayerFunction = (
|
|
45
|
-
export
|
|
46
|
-
export declare const XR_H_PREFIX = "h";
|
|
42
|
+
export type SignalingLayerFunction = (hooks: SignalingHooks) => Promise<SignalingLayer>;
|
|
43
|
+
export type CreateSignalingLayerFunction = (base: SignalingChannel) => SignalingLayerFunction;
|
|
47
44
|
/**
|
|
48
45
|
* Base Signaling Layer implementation
|
|
49
46
|
*
|
|
50
47
|
* https://openlv.sh/api/signaling
|
|
51
48
|
*/
|
|
52
|
-
export declare const createSignalingLayer:
|
|
49
|
+
export declare const createSignalingLayer: CreateSignalingLayerFunction;
|