@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.
@@ -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 SignalingStage = typeof XR_PREFIX | typeof XR_H_PREFIX;
6
- /**
7
- * Relays are lossy (MQTT QoS 0, ntfy best-effort), so every handshake step
8
- * is re-sent on an interval until the state machine observes progress.
9
- * Receivers treat duplicates as no-ops, which keeps re-sends wire-compatible.
10
- */
11
- export type HandshakeHooks = {
12
- isHost: boolean;
13
- handshakeKey: SymmetricKey;
14
- encrypt: (payload: string) => Promise<string>;
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 handshake = (hooks) => {
6
- const { isHost, handshakeKey, encrypt, decrypt, } = hooks;
7
- const frame = async (method, payload) => {
8
- const [prefix, message] = await match(method)
9
- .with("handshake", async () => [XR_H_PREFIX, await handshakeKey.encrypt(JSON.stringify(payload))])
10
- .with("encrypted", async () => [XR_PREFIX, await encrypt(JSON.stringify(payload))])
11
- .exhaustive();
12
- if (message === undefined) {
13
- throw new Error(`Cannot encrypt ${method} frame: key not available`);
14
- }
15
- const recipient = isHost ? "c" : "h";
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
@@ -1 +1 @@
1
- {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAEnC,OAAO,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAC;AAEvE,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAkB/B,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,KAAqB,EAAE,EAAE;IACjD,MAAM,EACJ,MAAM,EACN,YAAY,EACZ,OAAO,EACP,OAAO,GACR,GAAG,KAAK,CAAC;IAEV,MAAM,KAAK,GAAG,KAAK,EAAE,MAAiC,EAAE,OAAsB,EAAE,EAAE;QAChF,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;aAC1C,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,MAAM,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;aACjG,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;aAClF,UAAU,EAAE,CAAC;QAEhB,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,2BAA2B,CAAC,CAAC;QACvE,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QAErC,OAAO,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACtC,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,KAAK,EAAE,OAAe,EAAwD,EAAE;QAC5F,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAmB,CAAC;QACrD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACtC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,WAAW,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC;QAEvD,IAAI,CAAC,WAAW;YAAE,OAAO;QAEzB,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC;aAChC,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;aAC/D,IAAI,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;aAChD,SAAS,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAE9B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,MAAM,OAAO,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAE5C,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC,CAAC","sourcesContent":["import type { SymmetricKey } from \"@openlv/core/encryption\";\nimport { match } from \"ts-pattern\";\n\nimport { parseSignalMessage, type SignalMessage } from \"./messages.js\";\n\nexport const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\nexport type SignalingStage = typeof XR_PREFIX | typeof XR_H_PREFIX;\n\n/**\n * Relays are lossy (MQTT QoS 0, ntfy best-effort), so every handshake step\n * is re-sent on an interval until the state machine observes progress.\n * Receivers treat duplicates as no-ops, which keeps re-sends wire-compatible.\n */\n// const HANDSHAKE_RESEND_INTERVAL_MS = 2000;\n// const HANDSHAKE_TIMEOUT_MS = 30_000;\n\nexport type HandshakeHooks = {\n isHost: boolean;\n handshakeKey: SymmetricKey;\n encrypt: (payload: string) => Promise<string>;\n decrypt: (payload: string) => Promise<string>;\n};\n\nexport const handshake = (hooks: HandshakeHooks) => {\n const {\n isHost,\n handshakeKey,\n encrypt,\n decrypt,\n } = hooks;\n\n const frame = async (method: \"handshake\" | \"encrypted\", payload: SignalMessage) => {\n const [prefix, message] = await match(method)\n .with(\"handshake\", async () => [XR_H_PREFIX, await handshakeKey.encrypt(JSON.stringify(payload))])\n .with(\"encrypted\", async () => [XR_PREFIX, await encrypt(JSON.stringify(payload))])\n .exhaustive();\n\n if (message === undefined) {\n throw new Error(`Cannot encrypt ${method} frame: key not available`);\n }\n\n const recipient = isHost ? \"c\" : \"h\";\n\n return prefix + recipient + message;\n };\n\n const parse = async (payload: string): Promise<[SignalingStage, SignalMessage] | undefined> => {\n const prefix = payload.slice(0, 1) as SignalingStage;\n const recipient = payload.slice(1, 2);\n const body = payload.slice(2);\n const isRecipient = (isHost ? \"h\" : \"c\") === recipient;\n\n if (!isRecipient) return;\n\n const content = await match(prefix)\n .with(XR_H_PREFIX, async () => await handshakeKey.decrypt(body))\n .with(XR_PREFIX, async () => await decrypt(body))\n .otherwise(() => undefined);\n\n if (!content) return;\n\n const message = parseSignalMessage(content);\n\n if (!message) return;\n\n return [prefix, message];\n };\n\n return { frame, parse };\n};\n"]}
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"]}
@@ -1,6 +1,7 @@
1
- import { deriveSymmetricKey, generateKeyPair, hashPublicKey, } from "@openlv/core/encryption";
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 published = 0;
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 = published++;
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
- subscriber(payload);
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
- k: await deriveSymmetricKey(k),
56
- publicKey: encryptionKey,
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, encryptionKey, getPeerCapabilities: () => peerCapabilities };
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 = await generateKeyPair();
79
- const h = await hashPublicKey(hostKeys.encryptionKey);
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
- k: await deriveSymmetricKey(K),
90
- publicKey: hostKeys.encryptionKey,
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
- // Skipped until handshake resend/timeout logic is (re)implemented.
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("recovers when handshake frames are dropped", async () => {
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
- if (seen.has(payload.slice(0, 2) + payload.length))
292
+ const key = payload.slice(0, 2) + payload.length;
293
+ if (seen.has(key))
199
294
  return false;
200
- seen.add(payload.slice(0, 2) + payload.length);
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 = Promise.all([
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 vi.advanceTimersByTimeAsync(2100);
303
+ await advance(2100);
213
304
  }
214
305
  await encrypted;
215
306
  });
216
- it("errors out when the peer never appears", async () => {
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
- const errored = waitForState(client, Status.ERROR);
222
- await vi.advanceTimersByTimeAsync(31000);
223
- await errored;
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 { EncryptionKey, SymmetricKey } from "@openlv/core/encryption";
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
- k: SymmetricKey;
24
+ handshakeCipher: Cipher;
25
+ keyPair: KeyPair;
25
26
  capabilities: PeerCapabilities;
26
- decrypt: (message: string) => Promise<string>;
27
- encrypt: (message: string) => Promise<string>;
28
- publicKey: EncryptionKey;
29
- canEncrypt: () => boolean;
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
- peerKey: Observable<EncryptionKey | undefined>;
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 { parseEncryptionKey, validatePublicKeyHash } from "@openlv/core/encryption";
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 { handshake, XR_H_PREFIX, XR_PREFIX } from "./handshake.js";
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 { canEncrypt, encrypt, decrypt, capabilities, h, k: handshakeKey, publicKey, isHost, } = hooks;
25
+ const { capabilities, h, handshakeCipher, keyPair, isHost, } = hooks;
26
26
  const [status, setStatus] = observable(Status.STANDBY);
27
- const [peerKey, setPeerKey] = observable(undefined);
27
+ const [peer, setPeer] = observable(undefined);
28
28
  const [peerCapabilities, setPeerCapabilities] = observable(undefined);
29
29
  const emitter = new EventEmitter();
30
- const { frame, parse } = handshake({
31
- isHost,
32
- handshakeKey,
33
- encrypt,
34
- decrypt,
35
- });
36
- const send = async (method, payload) => await channel.publish(await frame(method, payload));
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.toString() },
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.HANDSHAKE);
51
- await send("handshake", pubkeyMessage());
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
- let receivedKey;
55
- try {
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("Failed to parse received host public key");
70
+ log("Received host public key is malformed or does not match expected hash -- possible tampering");
66
71
  return;
67
72
  }
68
- const changed = setPeerKey(receivedKey)
69
- && setStatus(Status.HANDSHAKE_PARTIAL);
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 send("encrypted", capabilitiesMessage());
103
+ return await publish(XR_PREFIX, capabilitiesMessage());
94
104
  })
95
- // Client already encrypted, but the host is still re-sending its
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 (payload) => {
113
+ const onReceive = async (frame) => {
107
114
  try {
108
- const parsed = await parse(payload);
109
- if (!parsed)
115
+ const parsed = parseFrame(frame);
116
+ if (parsed?.recipient !== (isHost ? "h" : "c"))
110
117
  return;
111
- const [stage, message] = parsed;
112
- if (stage === XR_H_PREFIX && message)
113
- return await handleHandshakeFrame(message);
114
- if (stage === XR_PREFIX && message)
115
- return await handleEncryptedFrame(message);
116
- log("Dropping frame with unknown prefix");
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
- // Enter HANDSHAKE before publishing: the host's pubkey reply can
133
- // arrive while the publish is still in flight.
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
- peerKey,
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"]}
@@ -1,9 +1,9 @@
1
- import { deriveSymmetricKey, generateKeyPair } from "@openlv/core/encryption";
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: "ws://test.mosquitto.org:8080/mqtt",
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 { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();
75
- const h = hKey;
76
- const k = await deriveSymmetricKey(K);
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
- k,
80
+ handshakeCipher,
81
81
  capabilities: { transports: ["wrtc"] },
82
- canEncrypt: () => true,
83
- encrypt: publicKey.encrypt,
84
- decrypt: decryptionKey.decrypt,
85
- publicKey,
82
+ keyPair: hostKeys,
86
83
  isHost: true,
87
84
  });
88
- signalA.peerKey.subscribe(v => log("rpKey", v));
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
- k,
89
+ handshakeCipher,
93
90
  capabilities: { transports: ["wrtc"] },
94
- canEncrypt: () => true,
95
- encrypt: publicKey.encrypt,
96
- decrypt: decryptionKey.decrypt,
97
- publicKey,
91
+ keyPair: generateKeyPair(),
98
92
  isHost: false,
99
93
  });
100
- signalB.peerKey.subscribe(v => log("rpKey", v));
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 Promise.all([signalA.setup(), signalB.setup()]);
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);
@@ -1 +1 @@
1
- {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","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":"AAAA,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAChG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpC,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,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,oCAAoC;iBAC1C;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,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAClD,MAAM,eAAe,GAAG,MAAM,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAEvD,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC;QACnC,CAAC;QACD,eAAe;QACf,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;QACtC,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE,IAAI;KACb,CAAC,CAAC;IAEH,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,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,eAAe;QACf,YAAY,EAAE,EAAE,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE;QACtC,OAAO,EAAE,eAAe,EAAE;QAC1B,MAAM,EAAE,KAAK;KACd,CAAC,CAAC;IAEH,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,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,KAAK,EAAE,CAAC;IACtB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACtB,MAAM,OAAO,CAAC,GAAG,CAAC;QAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC,SAAS,CAAC;QACzD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC,SAAS,CAAC;KAC1D,CAAC,CAAC;IAEH,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 { 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"]}
@@ -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() {
@@ -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,8DAA8D;oBAC9D,iEAAiE;oBACjE,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,UAAU,CAAC,OAAO,EAAE,CAAC;YACvB,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 // The client reports transport failures as a DOM Event, which\n // stringifies to \"[object Event]\" and reaches the user's screen.\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 connection.connect();\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"]}
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.2.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.2.0"
57
+ "@openlv/core": "0.3.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "typescript": "^5",