@openlv/signaling 0.2.0 → 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/handshake.d.ts +10 -18
- package/dist/handshake.js +11 -34
- package/dist/handshake.js.map +1 -1
- package/dist/handshake.test.js +186 -51
- package/dist/handshake.test.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.js +73 -82
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +18 -19
- package/dist/index.test.js.map +1 -1
- package/dist/mqtt/index.js +1 -3
- package/dist/mqtt/index.js.map +1 -1
- package/package.json +2 -2
package/dist/handshake.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
import type { SymmetricKey } from "@openlv/core/encryption";
|
|
2
|
-
import { type SignalMessage } from "./messages.js";
|
|
3
1
|
export declare const XR_PREFIX = "x";
|
|
4
2
|
export declare const XR_H_PREFIX = "h";
|
|
5
|
-
export type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
decrypt: (payload: string) => Promise<string>;
|
|
16
|
-
};
|
|
17
|
-
export declare const handshake: (hooks: HandshakeHooks) => {
|
|
18
|
-
frame: (method: "handshake" | "encrypted", payload: SignalMessage) => Promise<string>;
|
|
19
|
-
parse: (payload: string) => Promise<[SignalingStage, SignalMessage] | undefined>;
|
|
20
|
-
};
|
|
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;
|
package/dist/handshake.js
CHANGED
|
@@ -1,38 +1,15 @@
|
|
|
1
|
-
import { match } from "ts-pattern";
|
|
2
|
-
import { parseSignalMessage } from "./messages.js";
|
|
3
1
|
export const XR_PREFIX = "x";
|
|
4
2
|
export const XR_H_PREFIX = "h";
|
|
5
|
-
export const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return prefix + recipient + message;
|
|
17
|
-
};
|
|
18
|
-
const parse = async (payload) => {
|
|
19
|
-
const prefix = payload.slice(0, 1);
|
|
20
|
-
const recipient = payload.slice(1, 2);
|
|
21
|
-
const body = payload.slice(2);
|
|
22
|
-
const isRecipient = (isHost ? "h" : "c") === recipient;
|
|
23
|
-
if (!isRecipient)
|
|
24
|
-
return;
|
|
25
|
-
const content = await match(prefix)
|
|
26
|
-
.with(XR_H_PREFIX, async () => await handshakeKey.decrypt(body))
|
|
27
|
-
.with(XR_PREFIX, async () => await decrypt(body))
|
|
28
|
-
.otherwise(() => undefined);
|
|
29
|
-
if (!content)
|
|
30
|
-
return;
|
|
31
|
-
const message = parseSignalMessage(content);
|
|
32
|
-
if (!message)
|
|
33
|
-
return;
|
|
34
|
-
return [prefix, message];
|
|
35
|
-
};
|
|
36
|
-
return { frame, parse };
|
|
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) };
|
|
37
14
|
};
|
|
38
15
|
//# sourceMappingURL=handshake.js.map
|
package/dist/handshake.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"
|
|
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
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, Status } 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,60 +55,37 @@ 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),
|
|
62
|
+
handshakeCipher: await createHandshakeCipher(k),
|
|
63
|
+
keyPair: generateKeyPair(),
|
|
64
64
|
capabilities: CLIENT_CAPABILITIES,
|
|
65
65
|
});
|
|
66
|
-
layer.peerKey.subscribe((key) => {
|
|
67
|
-
relyingKey = key;
|
|
68
|
-
});
|
|
69
66
|
layer.peerCapabilities.subscribe(async (capabilities) => {
|
|
70
67
|
peerCapabilities = capabilities;
|
|
71
68
|
});
|
|
72
|
-
return { layer,
|
|
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),
|
|
85
|
+
handshakeCipher: await createHandshakeCipher(K),
|
|
86
|
+
keyPair: hostKeys,
|
|
98
87
|
capabilities: HOST_CAPABILITIES,
|
|
99
88
|
});
|
|
100
|
-
host.peerKey.subscribe((key) => {
|
|
101
|
-
hostRelying = key;
|
|
102
|
-
});
|
|
103
89
|
host.peerCapabilities.subscribe((capabilities) => {
|
|
104
90
|
hostPeerCapabilities = capabilities;
|
|
105
91
|
});
|
|
@@ -128,6 +114,26 @@ const waitForState = (layer, target) => new Promise((resolve, reject) => {
|
|
|
128
114
|
}
|
|
129
115
|
});
|
|
130
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
|
+
};
|
|
131
137
|
describe("signaling handshake (in-memory relay)", () => {
|
|
132
138
|
let host;
|
|
133
139
|
let client;
|
|
@@ -182,46 +188,175 @@ describe("signaling handshake (in-memory relay)", () => {
|
|
|
182
188
|
topic.inject("hcgarbage-mid-handshake");
|
|
183
189
|
await encrypted;
|
|
184
190
|
});
|
|
185
|
-
|
|
186
|
-
describe.skip("lossy relay", () => {
|
|
191
|
+
describe("timeout", () => {
|
|
187
192
|
beforeEach(() => {
|
|
188
193
|
vi.useFakeTimers();
|
|
189
194
|
});
|
|
190
195
|
afterEach(() => {
|
|
191
196
|
vi.useRealTimers();
|
|
192
197
|
});
|
|
193
|
-
it("
|
|
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", () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
vi.useFakeTimers();
|
|
273
|
+
});
|
|
274
|
+
afterEach(() => {
|
|
275
|
+
vi.useRealTimers();
|
|
276
|
+
});
|
|
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 () => {
|
|
194
289
|
const topic = createTopic();
|
|
195
|
-
// Drop the first delivery of every frame; re-sends get through.
|
|
196
290
|
const seen = new Set();
|
|
197
291
|
topic.setDrop((payload) => {
|
|
198
|
-
|
|
292
|
+
const key = payload.slice(0, 2) + payload.length;
|
|
293
|
+
if (seen.has(key))
|
|
199
294
|
return false;
|
|
200
|
-
seen.add(
|
|
295
|
+
seen.add(key);
|
|
201
296
|
return true;
|
|
202
297
|
});
|
|
203
298
|
({ host, client } = await setupPair(topic));
|
|
204
299
|
await host.setup();
|
|
205
300
|
await client.setup();
|
|
206
|
-
const encrypted =
|
|
207
|
-
waitForState(host, Status.ENCRYPTED),
|
|
208
|
-
waitForState(client, Status.ENCRYPTED),
|
|
209
|
-
]);
|
|
210
|
-
// Let several resend intervals elapse.
|
|
301
|
+
const encrypted = bothEncrypted(host, client);
|
|
211
302
|
for (let index = 0; index < 10; index += 1) {
|
|
212
|
-
await
|
|
303
|
+
await advance(2100);
|
|
213
304
|
}
|
|
214
305
|
await encrypted;
|
|
215
306
|
});
|
|
216
|
-
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 () => {
|
|
217
319
|
const topic = createTopic();
|
|
218
320
|
topic.setDrop(() => true);
|
|
219
321
|
({ host, client } = await setupPair(topic));
|
|
322
|
+
await host.setup();
|
|
220
323
|
await client.setup();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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);
|
|
224
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();
|
|
350
|
+
await client.setup();
|
|
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);
|
|
225
360
|
});
|
|
226
361
|
});
|
|
227
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} 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\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 capabilities: CLIENT_CAPABILITIES,\n });\n\n layer.peerKey.subscribe((key) => {\n relyingKey = key;\n });\n\n layer.peerCapabilities.subscribe(async (capabilities) => {\n peerCapabilities = capabilities;\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 capabilities: HOST_CAPABILITIES,\n });\n\n host.peerKey.subscribe((key) => {\n hostRelying = key;\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\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 // Skipped until handshake resend/timeout logic is (re)implemented.\n describe.skip(\"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, Status.ENCRYPTED),\n waitForState(client, Status.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, Status.ERROR);\n\n await vi.advanceTimersByTimeAsync(31_000);\n await errored;\n expect(client.status.get()).toBe(Status.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,5 +1,5 @@
|
|
|
1
1
|
import { type Observable } from "@openlv/core";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Cipher, KeyPair, PublicKey } from "@openlv/core/encryption";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
4
|
import { type PeerCapabilities } from "./messages.js";
|
|
5
5
|
import type { SignalingChannel } from "./protocol.js";
|
|
@@ -21,12 +21,13 @@ export type SignalEventMap = {
|
|
|
21
21
|
export type SignalingHooks = {
|
|
22
22
|
isHost: boolean;
|
|
23
23
|
h: string;
|
|
24
|
-
|
|
24
|
+
handshakeCipher: Cipher;
|
|
25
|
+
keyPair: KeyPair;
|
|
25
26
|
capabilities: PeerCapabilities;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
};
|
|
28
|
+
export type Peer = {
|
|
29
|
+
peerKey: PublicKey;
|
|
30
|
+
cipher: Cipher;
|
|
30
31
|
};
|
|
31
32
|
export type SignalingContext = {
|
|
32
33
|
type: string;
|
|
@@ -34,7 +35,7 @@ export type SignalingContext = {
|
|
|
34
35
|
setup: () => Promise<void>;
|
|
35
36
|
teardown: () => Promise<void>;
|
|
36
37
|
status: Observable<Status>;
|
|
37
|
-
|
|
38
|
+
peer: Observable<Peer | undefined>;
|
|
38
39
|
peerCapabilities: Observable<PeerCapabilities | undefined>;
|
|
39
40
|
};
|
|
40
41
|
export type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { make, observable } from "@openlv/core";
|
|
2
|
-
import {
|
|
1
|
+
import { createScope, make, observable } from "@openlv/core";
|
|
2
|
+
import { acceptPeer, createPeerCipher, hashPublicKey, parsePublicKey, toBase64, } from "@openlv/core/encryption";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
4
|
import { match } from "ts-pattern";
|
|
5
|
-
import {
|
|
6
|
-
import {} from "./messages.js";
|
|
5
|
+
import { composeFrame, HANDSHAKE_RESEND_INTERVAL_MS, HANDSHAKE_TIMEOUT_MS, parseFrame, XR_H_PREFIX, XR_PREFIX, } from "./handshake.js";
|
|
6
|
+
import { parseSignalMessage, } from "./messages.js";
|
|
7
7
|
import { log } from "./utils/log.js";
|
|
8
8
|
export * from "./messages.js";
|
|
9
9
|
export * from "./protocol.js";
|
|
@@ -22,18 +22,31 @@ export const Status = {
|
|
|
22
22
|
* https://openlv.sh/api/signaling
|
|
23
23
|
*/
|
|
24
24
|
export const createSignalingLayer = channel => async (hooks) => {
|
|
25
|
-
const {
|
|
25
|
+
const { capabilities, h, handshakeCipher, keyPair, isHost, } = hooks;
|
|
26
26
|
const [status, setStatus] = observable(Status.STANDBY);
|
|
27
|
-
const [
|
|
27
|
+
const [peer, setPeer] = observable(undefined);
|
|
28
28
|
const [peerCapabilities, setPeerCapabilities] = observable(undefined);
|
|
29
29
|
const emitter = new EventEmitter();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const scope = createScope();
|
|
31
|
+
scope.add(channel.teardown);
|
|
32
|
+
const handshake = scope.child();
|
|
33
|
+
handshake.closeWhen(status, value => value === Status.ENCRYPTED || value === Status.ERROR);
|
|
34
|
+
const publish = async (prefix, message) => {
|
|
35
|
+
const cipher = prefix === XR_H_PREFIX ? handshakeCipher : peer.get()?.cipher;
|
|
36
|
+
if (!cipher)
|
|
37
|
+
throw new Error("Cannot encrypt message before keys are exchanged");
|
|
38
|
+
const ciphertext = await cipher.encrypt(JSON.stringify(message));
|
|
39
|
+
await channel.publish(composeFrame(prefix, isHost ? "c" : "h", ciphertext));
|
|
40
|
+
};
|
|
41
|
+
const enterStep = (from, next, prefix, message, nextPeer = peer.get()) => {
|
|
42
|
+
if (status.get() !== from)
|
|
43
|
+
return;
|
|
44
|
+
setPeer(nextPeer);
|
|
45
|
+
setStatus(next);
|
|
46
|
+
const step = handshake.child();
|
|
47
|
+
step.closeWhen(status, value => value !== next);
|
|
48
|
+
step.repeat(HANDSHAKE_RESEND_INTERVAL_MS, () => publish(prefix, message));
|
|
49
|
+
};
|
|
37
50
|
const capabilitiesMessage = () => ({
|
|
38
51
|
type: "capabilities",
|
|
39
52
|
payload: capabilities,
|
|
@@ -41,61 +54,55 @@ export const createSignalingLayer = channel => async (hooks) => {
|
|
|
41
54
|
});
|
|
42
55
|
const pubkeyMessage = () => ({
|
|
43
56
|
type: "pubkey",
|
|
44
|
-
payload: { publicKey: publicKey
|
|
57
|
+
payload: { publicKey: toBase64(keyPair.publicKey) },
|
|
45
58
|
timestamp: Date.now(),
|
|
46
59
|
});
|
|
47
60
|
const handleHandshakeFrame = async (message) => {
|
|
48
61
|
await match({ msg: message, status: status.get(), isHost })
|
|
49
62
|
.with({ msg: { type: "flash" }, status: Status.READY, isHost: true }, async () => {
|
|
50
|
-
setStatus(Status.
|
|
51
|
-
|
|
63
|
+
handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));
|
|
64
|
+
enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, pubkeyMessage());
|
|
52
65
|
})
|
|
53
66
|
.with({ msg: { type: "pubkey" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
receivedKey = await parseEncryptionKey(messagePayload.publicKey);
|
|
57
|
-
if (!await validatePublicKeyHash(receivedKey, h)) {
|
|
58
|
-
setStatus(Status.ERROR);
|
|
59
|
-
log("Received host public key does not match expected hash -- possible tampering");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
67
|
+
const peerKey = parsePublicKey(messagePayload.publicKey);
|
|
68
|
+
if (!peerKey || await hashPublicKey(peerKey) !== h) {
|
|
64
69
|
setStatus(Status.ERROR);
|
|
65
|
-
log("
|
|
70
|
+
log("Received host public key is malformed or does not match expected hash -- possible tampering");
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
if (!changed)
|
|
71
|
-
return;
|
|
72
|
-
return await send("encrypted", pubkeyMessage());
|
|
73
|
+
const cipher = await createPeerCipher(keyPair, peerKey);
|
|
74
|
+
enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, pubkeyMessage(), { peerKey, cipher });
|
|
73
75
|
})
|
|
74
76
|
.otherwise(() => {
|
|
75
77
|
log("Ignoring handshake frame", message.type, "in status", status.get());
|
|
76
78
|
});
|
|
77
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* The host has no peer key yet when the joining peer's `pubkey` arrives, so
|
|
82
|
+
* the key in the frame is the only one it can check the announced key against.
|
|
83
|
+
*/
|
|
84
|
+
const handleAcceptedPeer = async ({ peerKey, cipher, message }) => {
|
|
85
|
+
const signalMessage = parseSignalMessage(message);
|
|
86
|
+
if (!isHost || status.get() !== Status.HANDSHAKE || signalMessage?.type !== "pubkey") {
|
|
87
|
+
log("Ignoring first peer-key frame in status", status.get());
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (signalMessage.payload.publicKey !== toBase64(peerKey)) {
|
|
91
|
+
log("Ignoring pubkey that differs from the frame key");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, capabilitiesMessage(), { peerKey, cipher });
|
|
95
|
+
};
|
|
78
96
|
const handleEncryptedFrame = async (message) => {
|
|
79
97
|
await match({ msg: message, status: status.get(), isHost })
|
|
80
|
-
// Host: client responded with its public key.
|
|
81
|
-
.with({ msg: { type: "pubkey" }, isHost: true, status: Status.HANDSHAKE }, async ({ msg: { payload: messagePayload } }) => {
|
|
82
|
-
const receivedKey = await parseEncryptionKey(messagePayload.publicKey);
|
|
83
|
-
if (!setPeerKey(receivedKey))
|
|
84
|
-
return;
|
|
85
|
-
setStatus(Status.HANDSHAKE_PARTIAL);
|
|
86
|
-
return await send("encrypted", capabilitiesMessage());
|
|
87
|
-
})
|
|
88
98
|
.with({ msg: { type: "capabilities" }, status: Status.HANDSHAKE_PARTIAL }, async ({ msg: { payload: messagePayload } }) => {
|
|
89
99
|
await setPeerCapabilities(messagePayload);
|
|
90
100
|
setStatus(Status.ENCRYPTED);
|
|
91
101
|
if (isHost)
|
|
92
102
|
return;
|
|
93
|
-
return await
|
|
103
|
+
return await publish(XR_PREFIX, capabilitiesMessage());
|
|
94
104
|
})
|
|
95
|
-
|
|
96
|
-
// capabilities (our final packet was lost): answer again so the host
|
|
97
|
-
// can finish.
|
|
98
|
-
.with({ msg: { type: "capabilities" }, status: Status.ENCRYPTED, isHost: false }, async () => await send("encrypted", capabilitiesMessage()))
|
|
105
|
+
.with({ msg: { type: "capabilities" }, status: Status.ENCRYPTED, isHost: false }, async () => await publish(XR_PREFIX, capabilitiesMessage()))
|
|
99
106
|
.with({ msg: { type: "data" }, status: Status.ENCRYPTED }, async () => {
|
|
100
107
|
emitter.emit("message", message.payload);
|
|
101
108
|
})
|
|
@@ -103,17 +110,24 @@ export const createSignalingLayer = channel => async (hooks) => {
|
|
|
103
110
|
log("Ignoring encrypted frame", message.type, "in status", status.get());
|
|
104
111
|
});
|
|
105
112
|
};
|
|
106
|
-
const onReceive = async (
|
|
113
|
+
const onReceive = async (frame) => {
|
|
107
114
|
try {
|
|
108
|
-
const parsed =
|
|
109
|
-
if (
|
|
115
|
+
const parsed = parseFrame(frame);
|
|
116
|
+
if (parsed?.recipient !== (isHost ? "h" : "c"))
|
|
110
117
|
return;
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
const { prefix, ciphertext } = parsed;
|
|
119
|
+
if (prefix === XR_H_PREFIX) {
|
|
120
|
+
const message = parseSignalMessage(await handshakeCipher.decrypt(ciphertext));
|
|
121
|
+
if (message)
|
|
122
|
+
await handleHandshakeFrame(message);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const current = peer.get();
|
|
126
|
+
if (!current)
|
|
127
|
+
return await handleAcceptedPeer(await acceptPeer(keyPair, ciphertext));
|
|
128
|
+
const message = parseSignalMessage(await current.cipher.decrypt(ciphertext));
|
|
129
|
+
if (message)
|
|
130
|
+
await handleEncryptedFrame(message);
|
|
117
131
|
}
|
|
118
132
|
catch (error) {
|
|
119
133
|
log("Dropping undecryptable or malformed frame", error);
|
|
@@ -123,42 +137,19 @@ export const createSignalingLayer = channel => async (hooks) => {
|
|
|
123
137
|
setStatus(Status.CONNECTING);
|
|
124
138
|
await channel.setup();
|
|
125
139
|
await channel.subscribe(onReceive);
|
|
126
|
-
if (canEncrypt()) {
|
|
127
|
-
setStatus(Status.ENCRYPTED);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
140
|
setStatus(Status.READY);
|
|
131
141
|
if (!isHost) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
setStatus(Status.HANDSHAKE);
|
|
135
|
-
await send("handshake", {
|
|
136
|
-
type: "flash",
|
|
137
|
-
payload: {},
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
});
|
|
142
|
+
handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));
|
|
143
|
+
enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, { type: "flash", payload: {}, timestamp: Date.now() });
|
|
140
144
|
}
|
|
141
145
|
};
|
|
142
|
-
const teardown = async () => {
|
|
143
|
-
log("teardown");
|
|
144
|
-
await channel.teardown?.();
|
|
145
|
-
};
|
|
146
146
|
return make(emitter, {
|
|
147
147
|
type: channel.type,
|
|
148
148
|
setup,
|
|
149
|
-
teardown,
|
|
150
|
-
send(message) {
|
|
151
|
-
if (!canEncrypt()) {
|
|
152
|
-
return Promise.reject(new Error("Cannot encrypt message before keys are exchanged"));
|
|
153
|
-
}
|
|
154
|
-
return send("encrypted", {
|
|
155
|
-
type: "data",
|
|
156
|
-
payload: message,
|
|
157
|
-
timestamp: Date.now(),
|
|
158
|
-
});
|
|
159
|
-
},
|
|
149
|
+
teardown: () => scope.close(),
|
|
150
|
+
send: (message) => publish(XR_PREFIX, { type: "data", payload: message, timestamp: Date.now() }),
|
|
160
151
|
status,
|
|
161
|
-
|
|
152
|
+
peer,
|
|
162
153
|
peerCapabilities,
|
|
163
154
|
});
|
|
164
155
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { make, type Observable, observable } from \"@openlv/core\";\nimport type { EncryptionKey, SymmetricKey } from \"@openlv/core/encryption\";\nimport { parseEncryptionKey, validatePublicKeyHash } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport { handshake, XR_H_PREFIX, XR_PREFIX } from \"./handshake.js\";\nimport {\n type PeerCapabilities,\n type SignalMessage,\n} from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./messages.js\";\nexport * from \"./protocol.js\";\n\nexport const Status = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type Status = (typeof Status)[keyof typeof Status];\n\nexport type SignalEventMap = {\n message: (message: object) => void;\n};\n\nexport type SignalingHooks = {\n isHost: boolean;\n h: string;\n k: SymmetricKey;\n // Our capabilities, advertised to the peer during the handshake\n capabilities: PeerCapabilities;\n // Decrypt using our private key\n decrypt: (message: string) => Promise<string>;\n // Encrypt to relying party\n encrypt: (message: string) => Promise<string>;\n // our public key\n publicKey: EncryptionKey;\n canEncrypt: () => boolean;\n};\n\nexport type SignalingContext = {\n type: string;\n\n // Sending only works once keys are exchanged\n send: (message: object) => Promise<void>;\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n\n status: Observable<Status>;\n peerKey: Observable<EncryptionKey | undefined>;\n peerCapabilities: Observable<PeerCapabilities | undefined>;\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFunction = (\n hooks: SignalingHooks,\n) => Promise<SignalingLayer>;\n\nexport type CreateSignalingLayerFunction = (\n base: SignalingChannel,\n) => SignalingLayerFunction;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer: CreateSignalingLayerFunction = channel => async (hooks: SignalingHooks) => {\n const {\n canEncrypt,\n encrypt,\n decrypt,\n capabilities,\n h,\n k: handshakeKey,\n publicKey,\n isHost,\n } = hooks;\n\n const [status, setStatus] = observable<Status>(Status.STANDBY);\n const [peerKey, setPeerKey] = observable<EncryptionKey | undefined>(undefined);\n const [peerCapabilities, setPeerCapabilities] = observable<PeerCapabilities | undefined>(undefined);\n\n const emitter = new EventEmitter<SignalEventMap>();\n\n const { frame, parse } = handshake({\n isHost,\n handshakeKey,\n encrypt,\n decrypt,\n });\n\n const send = async (method: \"handshake\" | \"encrypted\", payload: SignalMessage) => await channel.publish(await frame(method, payload));\n\n const capabilitiesMessage = (): SignalMessage => ({\n type: \"capabilities\",\n payload: capabilities,\n timestamp: Date.now(),\n });\n const pubkeyMessage = (): SignalMessage => ({\n type: \"pubkey\",\n payload: { publicKey: publicKey.toString() },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with({ msg: { type: \"flash\" }, status: Status.READY, isHost: true }, async () => {\n setStatus(Status.HANDSHAKE);\n await send(\"handshake\", pubkeyMessage());\n })\n .with({ msg: { type: \"pubkey\" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {\n let receivedKey: EncryptionKey;\n\n try {\n receivedKey = await parseEncryptionKey(messagePayload.publicKey);\n\n if (!await validatePublicKeyHash(receivedKey, h)) {\n setStatus(Status.ERROR);\n log(\"Received host public key does not match expected hash -- possible tampering\");\n\n return;\n }\n }\n catch {\n setStatus(Status.ERROR);\n log(\"Failed to parse received host public key\");\n\n return;\n }\n\n const changed = setPeerKey(receivedKey)\n && setStatus(Status.HANDSHAKE_PARTIAL);\n\n if (!changed) return;\n\n return await send(\"encrypted\", pubkeyMessage());\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", message.type, \"in status\", status.get());\n });\n };\n\n const handleEncryptedFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n // Host: client responded with its public key.\n .with(\n { msg: { type: \"pubkey\" }, isHost: true, status: Status.HANDSHAKE },\n async ({ msg: { payload: messagePayload } }) => {\n const receivedKey = await parseEncryptionKey(messagePayload.publicKey);\n\n if (!setPeerKey(receivedKey)) return;\n\n setStatus(Status.HANDSHAKE_PARTIAL);\n\n return await send(\"encrypted\", capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, status: Status.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: messagePayload } }) => {\n await setPeerCapabilities(messagePayload);\n setStatus(Status.ENCRYPTED);\n\n if (isHost) return;\n\n return await send(\"encrypted\", capabilitiesMessage());\n },\n )\n // Client already encrypted, but the host is still re-sending its\n // capabilities (our final packet was lost): answer again so the host\n // can finish.\n .with(\n { msg: { type: \"capabilities\" }, status: Status.ENCRYPTED, isHost: false },\n async () => await send(\"encrypted\", capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, status: Status.ENCRYPTED }, async () => {\n emitter.emit(\"message\", message.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", message.type, \"in status\", status.get());\n });\n };\n\n const onReceive = async (payload: string) => {\n try {\n const parsed = await parse(payload);\n\n if (!parsed) return;\n\n const [stage, message] = parsed;\n\n if (stage === XR_H_PREFIX && message) return await handleHandshakeFrame(message);\n\n if (stage === XR_PREFIX && message) return await handleEncryptedFrame(message);\n\n log(\"Dropping frame with unknown prefix\");\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n const setup = async () => {\n setStatus(Status.CONNECTING);\n await channel.setup();\n await channel.subscribe(onReceive);\n\n if (canEncrypt()) {\n setStatus(Status.ENCRYPTED);\n\n return;\n }\n\n setStatus(Status.READY);\n\n if (!isHost) {\n // Enter HANDSHAKE before publishing: the host's pubkey reply can\n // arrive while the publish is still in flight.\n setStatus(Status.HANDSHAKE);\n await send(\"handshake\", {\n type: \"flash\",\n payload: {},\n timestamp: Date.now(),\n });\n }\n };\n\n const teardown = async () => {\n log(\"teardown\");\n await channel.teardown?.();\n };\n\n return make(emitter, {\n type: channel.type,\n setup,\n teardown,\n send(message: object) {\n if (!canEncrypt()) {\n return Promise.reject(\n new Error(\"Cannot encrypt message before keys are exchanged\"),\n );\n }\n\n return send(\"encrypted\", {\n type: \"data\",\n payload: message,\n timestamp: Date.now(),\n });\n },\n status,\n peerKey,\n peerCapabilities,\n });\n};\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { createScope, make, type Observable, observable } from \"@openlv/core\";\nimport type { AcceptedPeer, Cipher, KeyPair, PublicKey } from \"@openlv/core/encryption\";\nimport {\n acceptPeer,\n createPeerCipher,\n hashPublicKey,\n parsePublicKey,\n toBase64,\n} from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport {\n composeFrame,\n HANDSHAKE_RESEND_INTERVAL_MS,\n HANDSHAKE_TIMEOUT_MS,\n parseFrame,\n type SignalingPrefix,\n XR_H_PREFIX,\n XR_PREFIX,\n} from \"./handshake.js\";\nimport {\n parseSignalMessage,\n type PeerCapabilities,\n type SignalMessage,\n} from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./messages.js\";\nexport * from \"./protocol.js\";\n\nexport const Status = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type Status = (typeof Status)[keyof typeof Status];\n\nexport type SignalEventMap = {\n message: (message: object) => void;\n};\n\nexport type SignalingHooks = {\n isHost: boolean;\n h: string;\n handshakeCipher: Cipher;\n keyPair: KeyPair;\n capabilities: PeerCapabilities;\n};\n\nexport type Peer = {\n peerKey: PublicKey;\n cipher: Cipher;\n};\n\nexport type SignalingContext = {\n type: string;\n\n send: (message: object) => Promise<void>;\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n\n status: Observable<Status>;\n peer: Observable<Peer | undefined>;\n peerCapabilities: Observable<PeerCapabilities | undefined>;\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFunction = (\n hooks: SignalingHooks,\n) => Promise<SignalingLayer>;\n\nexport type CreateSignalingLayerFunction = (\n base: SignalingChannel,\n) => SignalingLayerFunction;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer: CreateSignalingLayerFunction = channel => async (hooks: SignalingHooks) => {\n const {\n capabilities,\n h,\n handshakeCipher,\n keyPair,\n isHost,\n } = hooks;\n\n const [status, setStatus] = observable<Status>(Status.STANDBY);\n const [peer, setPeer] = observable<Peer | undefined>(undefined);\n const [peerCapabilities, setPeerCapabilities] = observable<PeerCapabilities | undefined>(undefined);\n\n const emitter = new EventEmitter<SignalEventMap>();\n const scope = createScope();\n\n scope.add(channel.teardown);\n\n const handshake = scope.child();\n\n handshake.closeWhen(status, value => value === Status.ENCRYPTED || value === Status.ERROR);\n\n const publish = async (prefix: SignalingPrefix, message: SignalMessage) => {\n const cipher = prefix === XR_H_PREFIX ? handshakeCipher : peer.get()?.cipher;\n\n if (!cipher) throw new Error(\"Cannot encrypt message before keys are exchanged\");\n\n const ciphertext = await cipher.encrypt(JSON.stringify(message));\n\n await channel.publish(composeFrame(prefix, isHost ? \"c\" : \"h\", ciphertext));\n };\n\n const enterStep = (from: Status, next: Status, prefix: SignalingPrefix, message: SignalMessage, nextPeer = peer.get()) => {\n if (status.get() !== from) return;\n\n setPeer(nextPeer);\n setStatus(next);\n\n const step = handshake.child();\n\n step.closeWhen(status, value => value !== next);\n step.repeat(HANDSHAKE_RESEND_INTERVAL_MS, () => publish(prefix, message));\n };\n\n const capabilitiesMessage = (): SignalMessage => ({\n type: \"capabilities\",\n payload: capabilities,\n timestamp: Date.now(),\n });\n const pubkeyMessage = (): SignalMessage => ({\n type: \"pubkey\",\n payload: { publicKey: toBase64(keyPair.publicKey) },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with({ msg: { type: \"flash\" }, status: Status.READY, isHost: true }, async () => {\n handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));\n enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, pubkeyMessage());\n })\n .with({ msg: { type: \"pubkey\" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {\n const peerKey = parsePublicKey(messagePayload.publicKey);\n\n if (!peerKey || await hashPublicKey(peerKey) !== h) {\n setStatus(Status.ERROR);\n log(\"Received host public key is malformed or does not match expected hash -- possible tampering\");\n\n return;\n }\n\n const cipher = await createPeerCipher(keyPair, peerKey);\n\n enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, pubkeyMessage(), { peerKey, cipher });\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", message.type, \"in status\", status.get());\n });\n };\n\n /**\n * The host has no peer key yet when the joining peer's `pubkey` arrives, so\n * the key in the frame is the only one it can check the announced key against.\n */\n const handleAcceptedPeer = async ({ peerKey, cipher, message }: AcceptedPeer) => {\n const signalMessage = parseSignalMessage(message);\n\n if (!isHost || status.get() !== Status.HANDSHAKE || signalMessage?.type !== \"pubkey\") {\n log(\"Ignoring first peer-key frame in status\", status.get());\n\n return;\n }\n\n if (signalMessage.payload.publicKey !== toBase64(peerKey)) {\n log(\"Ignoring pubkey that differs from the frame key\");\n\n return;\n }\n\n enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, capabilitiesMessage(), { peerKey, cipher });\n };\n\n const handleEncryptedFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with(\n { msg: { type: \"capabilities\" }, status: Status.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: messagePayload } }) => {\n await setPeerCapabilities(messagePayload);\n setStatus(Status.ENCRYPTED);\n\n if (isHost) return;\n\n return await publish(XR_PREFIX, capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, status: Status.ENCRYPTED, isHost: false },\n async () => await publish(XR_PREFIX, capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, status: Status.ENCRYPTED }, async () => {\n emitter.emit(\"message\", message.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", message.type, \"in status\", status.get());\n });\n };\n\n const onReceive = async (frame: string) => {\n try {\n const parsed = parseFrame(frame);\n\n if (parsed?.recipient !== (isHost ? \"h\" : \"c\")) return;\n\n const { prefix, ciphertext } = parsed;\n\n if (prefix === XR_H_PREFIX) {\n const message = parseSignalMessage(await handshakeCipher.decrypt(ciphertext));\n\n if (message) await handleHandshakeFrame(message);\n\n return;\n }\n\n const current = peer.get();\n\n if (!current) return await handleAcceptedPeer(await acceptPeer(keyPair, ciphertext));\n\n const message = parseSignalMessage(await current.cipher.decrypt(ciphertext));\n\n if (message) await handleEncryptedFrame(message);\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n const setup = async () => {\n setStatus(Status.CONNECTING);\n await channel.setup();\n await channel.subscribe(onReceive);\n\n setStatus(Status.READY);\n\n if (!isHost) {\n handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));\n enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, { type: \"flash\", payload: {}, timestamp: Date.now() });\n }\n };\n\n return make(emitter, {\n type: channel.type,\n setup,\n teardown: () => scope.close(),\n send: (message: object) => publish(XR_PREFIX, { type: \"data\", payload: message, timestamp: Date.now() }),\n status,\n peer,\n peerCapabilities,\n });\n};\n"]}
|
package/dist/index.test.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHandshakeCipher, generateKeyPair, hashPublicKey } from "@openlv/core/encryption";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { Status } from "./index.js";
|
|
3
4
|
import { mqtt } from "./mqtt/index.js";
|
|
4
5
|
import { ntfy } from "./ntfy/index.js";
|
|
5
6
|
import { log } from "./utils/log.js";
|
|
6
|
-
const hKey = "test";
|
|
7
7
|
const K = "00112233445566778899aabbccddeeff";
|
|
8
8
|
const providersByType = [
|
|
9
9
|
[
|
|
@@ -30,7 +30,7 @@ const providersByType = [
|
|
|
30
30
|
mqtt,
|
|
31
31
|
{
|
|
32
32
|
topic: "mytesttopic1111",
|
|
33
|
-
url: "
|
|
33
|
+
url: "wss://test.mosquitto.org:8081/mqtt",
|
|
34
34
|
},
|
|
35
35
|
],
|
|
36
36
|
[
|
|
@@ -71,37 +71,36 @@ const providersByType = [
|
|
|
71
71
|
// ],
|
|
72
72
|
];
|
|
73
73
|
const testSignalingLayer = async (layer, properties) => {
|
|
74
|
-
const
|
|
75
|
-
const h =
|
|
76
|
-
const
|
|
74
|
+
const hostKeys = generateKeyPair();
|
|
75
|
+
const h = await hashPublicKey(hostKeys.publicKey);
|
|
76
|
+
const handshakeCipher = await createHandshakeCipher(K);
|
|
77
77
|
const signalingLayer = await layer(properties);
|
|
78
78
|
const signalA = await signalingLayer({
|
|
79
79
|
h,
|
|
80
|
-
|
|
80
|
+
handshakeCipher,
|
|
81
81
|
capabilities: { transports: ["wrtc"] },
|
|
82
|
-
|
|
83
|
-
encrypt: publicKey.encrypt,
|
|
84
|
-
decrypt: decryptionKey.decrypt,
|
|
85
|
-
publicKey,
|
|
82
|
+
keyPair: hostKeys,
|
|
86
83
|
isHost: true,
|
|
87
84
|
});
|
|
88
|
-
signalA.
|
|
85
|
+
signalA.peer.subscribe(v => log("rpKey", v?.peerKey));
|
|
89
86
|
signalA.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
90
87
|
const signalB = await signalingLayer({
|
|
91
88
|
h,
|
|
92
|
-
|
|
89
|
+
handshakeCipher,
|
|
93
90
|
capabilities: { transports: ["wrtc"] },
|
|
94
|
-
|
|
95
|
-
encrypt: publicKey.encrypt,
|
|
96
|
-
decrypt: decryptionKey.decrypt,
|
|
97
|
-
publicKey,
|
|
91
|
+
keyPair: generateKeyPair(),
|
|
98
92
|
isHost: false,
|
|
99
93
|
});
|
|
100
|
-
signalB.
|
|
94
|
+
signalB.peer.subscribe(v => log("rpKey", v?.peerKey));
|
|
101
95
|
signalB.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
102
96
|
expect(signalA).toBeDefined();
|
|
103
97
|
expect(signalB).toBeDefined();
|
|
104
|
-
await
|
|
98
|
+
await signalA.setup();
|
|
99
|
+
await signalB.setup();
|
|
100
|
+
await Promise.all([
|
|
101
|
+
signalA.status.until(state => state === Status.ENCRYPTED),
|
|
102
|
+
signalB.status.until(state => state === Status.ENCRYPTED),
|
|
103
|
+
]);
|
|
105
104
|
const messageReceivedA = new Promise((resolve) => {
|
|
106
105
|
signalA.on("message", (message) => {
|
|
107
106
|
log("messageReceivedA", message);
|
package/dist/index.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEvC,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAErC,MAAM,IAAI,GAAG,MAAM,CAAC;AACpB,MAAM,CAAC,GAAG,kCAAkC,CAAC;AAE7C,MAAM,eAAe,GAGf;IACJ;QACE,MAAM;QACN;YACE;gBACE,gBAAgB;gBAChB,IAAI;gBACJ;oBACE,KAAK,EAAE,iBAAiB;oBACxB,GAAG,EAAE,oCAAoC;iBAC1C;aACF;YACD;gBACE,gBAAgB;gBAChB,IAAI;gBACJ;oBACE,KAAK,EAAE,iBAAiB;oBACxB,GAAG,EAAE,+BAA+B;iBACrC;aACF;YACD;gBACE,oBAAoB;gBACpB,IAAI;gBACJ;oBACE,KAAK,EAAE,iBAAiB;oBACxB,GAAG,EAAE,mCAAmC;iBACzC;aACF;YACD;gBACE,kBAAkB;gBAClB,IAAI;gBACJ;oBACE,KAAK,EAAE,iBAAiB;oBACxB,GAAG,EAAE,6BAA6B;iBACnC;aACF;SACF;KACF;IACD;QACE,MAAM;QACN;YACE;gBACE,SAAS;gBACT,IAAI;gBACJ,EAAE,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,kBAAkB,EAAE;aACtD;YACD;gBACE,eAAe;gBACf,IAAI;gBACJ,EAAE,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,wBAAwB,EAAE;aAC5D;SACF;KACF;IACD,wEAAwE;IACxE,IAAI;IACJ,aAAa;IACb,MAAM;IACN,QAAQ;IACR,uBAAuB;IACvB,eAAe;IACf,oEAAoE;IACpE,SAAS;IACT,OAAO;IACP,KAAK;CACG,CAAC;AAEX,MAAM,kBAAkB,GAAG,KAAK,EAC9B,KAAwB,EACxB,UAAoC,EACrB,EAAE;IACjB,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,GAAG,MAAM,eAAe,EAAE,CAAC;IAC5E,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAEtC,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC;QACnC,CAAC;QACD,CAAC;QACD,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;QACtC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;QACtB,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,OAAO,EAAE,aAAa,CAAC,OAAO;QAC9B,SAAS;QACT,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;IAEH,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC;QACnC,CAAC;QACD,CAAC;QACD,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;QACtC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;QACtB,OAAO,EAAE,SAAS,CAAC,OAAO;QAC1B,OAAO,EAAE,aAAa,CAAC,OAAO;QAC9B,SAAS;QACT,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;IAEH,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC,CAAC,CAAC,CAAC;IAEpE,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAC9B,MAAM,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAE9B,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAEtD,MAAM,gBAAgB,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACrD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;YAChC,GAAG,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;YACjC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3C,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,MAAM,gBAAgB,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACrD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;YAChC,GAAG,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;YACjC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3C,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAEtC,MAAM,gBAAgB,CAAC;IAEvB,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;IAEtC,MAAM,gBAAgB,CAAC;IAEvB,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;IACzB,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAMjC,MAAM,YAAY,GAAG,KAAK,EACxB,KAAwB,EACxB,UAAoC,EACX,EAAE;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAC/C,UAAU,CACR,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,iBAAiB,mBAAmB,IAAI,CAAC,CAAC,EACjE,mBAAmB,CACpB,CACF,CAAC;IAEF,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAEpC,IAAI,CAAC;QACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,kBAAkB,CAAC,KAAK,EAAE,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;QACrE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC,CAAC;QAE3D,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IACrD,CAAC;IACD,OAAO,KAAK,EAAE,CAAC;QACb,OAAO;YACL,GAAG,EAAE,UAAU,CAAC,GAAG;YACnB,EAAE,EAAE,KAAK;YACT,KAAK,EAAG,KAAe,CAAC,OAAO;SAChC,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,QAAgB,EAAE,OAAyB,EAAU,EAAE;IAC5E,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;IAChD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC;IAE7B,OAAO;QACL,GAAG,QAAQ,CAAC,WAAW,EAAE,KAAK,MAAM,IAAI,KAAK,EAAE;QAC/C,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CACjB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAClE;KACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC,CAAC;AAEF,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC,QAAQ,EAAE,SAAS,CAAC,EAAE,EAAE;IACvE,EAAE,CACA,8CAA8C,EAC9C,EAAE,OAAO,EAAE,mBAAmB,GAAG,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,EAC1D,KAAK,IAAI,EAAE;QACT,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAC1E,CAAC;QAEF,MAAM,OAAO,GAAG,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAEjD,OAAO,CAAC,GAAG,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC;QAE5B,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC;QAEhD,MAAM,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CACF,CAAC;AACJ,CAAC,CAAC,CAAC","sourcesContent":["import { deriveSymmetricKey, generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst hKey = \"test\";\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://test.mosquitto.org:8080/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // gundb is deprecated and its public relays are too lossy to assert on.\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<void> => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const h = hKey;\n const k = await deriveSymmetricKey(K);\n\n const signalingLayer = await layer(properties);\n const signalA = await signalingLayer({\n h,\n k,\n capabilities: { transports: [\"wrtc\"] },\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: true,\n });\n\n signalA.peerKey.subscribe(v => log(\"rpKey\", v));\n signalA.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n const signalB = await signalingLayer({\n h,\n k,\n capabilities: { transports: [\"wrtc\"] },\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: false,\n });\n\n signalB.peerKey.subscribe(v => log(\"rpKey\", v));\n signalB.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await Promise.all([signalA.setup(), signalB.setup()]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult\n = | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, properties), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: properties.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: properties.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, properties]) => testProvider(layer, properties)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHandshakeCipher, generateKeyPair, hashPublicKey } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { Status } from \"./index.js\";\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://test.mosquitto.org:8081/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // gundb is deprecated and its public relays are too lossy to assert on.\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<void> => {\n const hostKeys = generateKeyPair();\n const h = await hashPublicKey(hostKeys.publicKey);\n const handshakeCipher = await createHandshakeCipher(K);\n\n const signalingLayer = await layer(properties);\n const signalA = await signalingLayer({\n h,\n handshakeCipher,\n capabilities: { transports: [\"wrtc\"] },\n keyPair: hostKeys,\n isHost: true,\n });\n\n signalA.peer.subscribe(v => log(\"rpKey\", v?.peerKey));\n signalA.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n const signalB = await signalingLayer({\n h,\n handshakeCipher,\n capabilities: { transports: [\"wrtc\"] },\n keyPair: generateKeyPair(),\n isHost: false,\n });\n\n signalB.peer.subscribe(v => log(\"rpKey\", v?.peerKey));\n signalB.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await signalA.setup();\n await signalB.setup();\n await Promise.all([\n signalA.status.until(state => state === Status.ENCRYPTED),\n signalB.status.until(state => state === Status.ENCRYPTED),\n ]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult\n = | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, properties), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: properties.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: properties.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, properties]) => testProvider(layer, properties)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
|
package/dist/mqtt/index.js
CHANGED
|
@@ -21,8 +21,6 @@ export const mqtt = ({ url, topic }) => {
|
|
|
21
21
|
});
|
|
22
22
|
connection.on("error", (error) => {
|
|
23
23
|
log("MQTT: Error connecting to URL", error);
|
|
24
|
-
// The client reports transport failures as a DOM Event, which
|
|
25
|
-
// stringifies to "[object Event]" and reaches the user's screen.
|
|
26
24
|
reject(error instanceof Error
|
|
27
25
|
? error
|
|
28
26
|
: new Error(`MQTT: could not connect to ${endpoint}`));
|
|
@@ -30,7 +28,7 @@ export const mqtt = ({ url, topic }) => {
|
|
|
30
28
|
connection.on("close", () => {
|
|
31
29
|
reject(new Error("MQTT: Closed connection to URL"));
|
|
32
30
|
});
|
|
33
|
-
connection.connect();
|
|
31
|
+
void connection.connect().catch(reject);
|
|
34
32
|
});
|
|
35
33
|
},
|
|
36
34
|
teardown() {
|
package/dist/mqtt/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mqtt/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAsB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;IACxD,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,GAAG,IAAI,oCAAoC,CAAC;IAC7D,IAAI,UAAkC,CAAC;IAEvC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,UAAU,GAAG,UAAU,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAE3C,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC5B,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;gBACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC/B,GAAG,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;oBAC5C,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/mqtt/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAmB,MAAM,gBAAgB,CAAC;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;GAIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAsB,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE;IACxD,+DAA+D;IAC/D,MAAM,QAAQ,GAAG,GAAG,IAAI,oCAAoC,CAAC;IAC7D,IAAI,UAAkC,CAAC;IAEvC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,MAAM;QACZ,KAAK;YACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;gBACrC,UAAU,GAAG,UAAU,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAE3C,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;oBAC5B,OAAO,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;gBACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBAC/B,GAAG,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;oBAC5C,MAAM,CACJ,KAAK,YAAY,KAAK;wBACpB,CAAC,CAAC,KAAK;wBACP,CAAC,CAAC,IAAI,KAAK,CAAC,8BAA8B,QAAQ,EAAE,CAAC,CACxD,CAAC;gBACJ,CAAC,CAAC,CAAC;gBAEH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;oBAC1B,MAAM,CAAC,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC,CAAC;gBACtD,CAAC,CAAC,CAAC;gBAEH,KAAK,UAAU,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC1C,CAAC,CAAC,CAAC;QACL,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,KAAK,EAAE,CAAC;YACpB,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;YACvD,CAAC;YAED,UAAU,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;YAEzC,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,aAAa,EAAE,OAAO,EAAE,EAAE;gBAClD,GAAG,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;gBAE9C,IAAI,aAAa,KAAK,KAAK;oBAAE,OAAO;gBAEpC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAElD,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;YAEH,MAAM,UAAU,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport { createMqtt, type MqttClient } from \"websocket-mqtt\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * MQTT Signaling Layer\n *\n * https://openlv.sh/api/signaling/mqtt\n */\nexport const mqtt: SignalingProtocol = ({ url, topic }) => {\n // `url` may be an empty string when the session URI omits `s`.\n const endpoint = url || \"wss://test.mosquitto.org:8081/mqtt\";\n let connection: MqttClient | undefined;\n\n return createSignalingLayer({\n type: \"mqtt\",\n setup() {\n return new Promise((resolve, reject) => {\n connection = createMqtt({ url: endpoint });\n\n connection.on(\"connect\", () => {\n resolve();\n });\n connection.on(\"error\", (error) => {\n log(\"MQTT: Error connecting to URL\", error);\n reject(\n error instanceof Error\n ? error\n : new Error(`MQTT: could not connect to ${endpoint}`),\n );\n });\n\n connection.on(\"close\", () => {\n reject(new Error(\"MQTT: Closed connection to URL\"));\n });\n\n void connection.connect().catch(reject);\n });\n },\n teardown() {\n connection?.close();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) {\n throw new Error(\"MQTT: No connection to publish to\");\n }\n\n connection?.publish(topic, payload, { retain: false });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"MQTT: Subscribing to topic\", topic);\n\n connection.on(\"message\", (receivedTopic, message) => {\n log(\"MQTT: Received message on topic\", topic);\n\n if (receivedTopic !== topic) return;\n\n const decoded = new TextDecoder().decode(message);\n\n handler(decoded);\n });\n\n await connection?.subscribe(topic);\n },\n });\n};\n\nObject.defineProperty(mqtt, \"__name\", { value: \"mqtt\" });\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlv/signaling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"open-lavatory"
|
|
6
6
|
],
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"gun": "^0.2020.1241",
|
|
55
55
|
"websocket-mqtt": "0.0.7",
|
|
56
56
|
"ts-pattern": "^5",
|
|
57
|
-
"@openlv/core": "0.
|
|
57
|
+
"@openlv/core": "0.3.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"typescript": "^5",
|