@openlv/signaling 0.1.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,5 +2,8 @@ import type { SignalingProtocol } from "../protocol.js";
2
2
  /**
3
3
  * GunDB Signaling Layer
4
4
  * https://openlv.sh/api/signaling/gun
5
+ *
6
+ * @deprecated Public gun relays drop frames often enough that a handshake
7
+ * fails regularly. Kept until a third backend replaces it.
5
8
  */
6
9
  export declare const gundb: SignalingProtocol;
@@ -5,6 +5,9 @@ import { log } from "../utils/log.js";
5
5
  /**
6
6
  * GunDB Signaling Layer
7
7
  * https://openlv.sh/api/signaling/gun
8
+ *
9
+ * @deprecated Public gun relays drop frames often enough that a handshake
10
+ * fails regularly. Kept until a third backend replaces it.
8
11
  */
9
12
  export const gundb = ({ topic, url }) => {
10
13
  const endpoint = url || "wss://try.axe.eco/gun";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gundb/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,GAAG,EAAE,EAAgC,MAAM,KAAK,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;IACzD,MAAM,QAAQ,GAAG,GAAG,IAAI,uBAAuB,CAAC;IAChD,IAAI,UAAoC,CAAC;IAEzC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,OAAO;QACb,KAAK,CAAC,KAAK;YACT,GAAG,CAAC,mBAAmB,CAAC,CAAC;YACzB,MAAM,CAAC,GAAG,IAAK,GAAuB,CAAC;gBACrC,KAAK,EAAE,CAAC,QAAQ,CAAC;gBACjB,IAAI,EAAE,SAAS;gBACf,YAAY,EAAE,SAAS;aACxB,CAAC,CAAC;YAEH,UAAU,GAAG,CAAC,CAAC;YAEf,MAAM,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;YAC7B,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,2BAA2B,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACjD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE;gBAChC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,CAAC;gBAErC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;gBAEtC,IAAI,CAAC,OAAO;oBAAE,OAAO;gBAErB,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;IACnB,MAAM,EAAE,OAAO;CAChB,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport Gun, { type IGun, type IGunInstance } from \"gun\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * GunDB Signaling Layer\n * https://openlv.sh/api/signaling/gun\n */\nexport const gundb: SignalingProtocol = ({ topic, url }) => {\n const endpoint = url || \"wss://try.axe.eco/gun\";\n let connection: IGunInstance | undefined;\n\n return createSignalingLayer({\n type: \"gundb\",\n async setup() {\n log(\"GUNDB: Setting up\");\n const x = new (Gun as unknown as IGun)({\n peers: [endpoint],\n file: undefined,\n localStorage: undefined,\n });\n\n connection = x;\n\n await connection?.get(topic);\n },\n teardown() {\n connection?.get(topic).off();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"GUNDB: Publishing message\", topic, payload);\n connection.get(topic).put({ data: payload });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n connection.get(topic).on((data) => {\n log(\"GUNDB: Received message\", data);\n\n const message = data.data?.toString();\n\n if (!message) return;\n\n handler(message);\n });\n },\n });\n};\n\nObject.assign(gundb, {\n __name: \"gundb\",\n});\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/gundb/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,GAAG,EAAE,EAAgC,MAAM,KAAK,CAAC;AAExD,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,KAAK,GAAsB,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,EAAE;IACzD,MAAM,QAAQ,GAAG,GAAG,IAAI,uBAAuB,CAAC;IAChD,IAAI,UAAoC,CAAC;IAEzC,OAAO,oBAAoB,CAAC;QAC1B,IAAI,EAAE,OAAO;QACb,KAAK,CAAC,KAAK;YACT,GAAG,CAAC,mBAAmB,CAAC,CAAC;YACzB,MAAM,CAAC,GAAG,IAAK,GAAuB,CAAC;gBACrC,KAAK,EAAE,CAAC,QAAQ,CAAC;gBACjB,IAAI,EAAE,SAAS;gBACf,YAAY,EAAE,SAAS;aACxB,CAAC,CAAC;YAEH,UAAU,GAAG,CAAC,CAAC;YAEf,MAAM,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC/B,CAAC;QACD,QAAQ;YACN,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC;YAC7B,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO;YACnB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,GAAG,CAAC,2BAA2B,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACjD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAO;YACrB,IAAI,CAAC,UAAU;gBAAE,MAAM,IAAI,uBAAuB,EAAE,CAAC;YAErD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE;gBAChC,GAAG,CAAC,yBAAyB,EAAE,IAAI,CAAC,CAAC;gBAErC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;gBAEtC,IAAI,CAAC,OAAO;oBAAE,OAAO;gBAErB,OAAO,CAAC,OAAO,CAAC,CAAC;YACnB,CAAC,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;IACnB,MAAM,EAAE,OAAO;CAChB,CAAC,CAAC","sourcesContent":["import { SignalNoConnectionError } from \"@openlv/core/errors\";\nimport Gun, { type IGun, type IGunInstance } from \"gun\";\n\nimport { createSignalingLayer } from \"../index.js\";\nimport type { SignalingProtocol } from \"../protocol.js\";\nimport { log } from \"../utils/log.js\";\n\n/**\n * GunDB Signaling Layer\n * https://openlv.sh/api/signaling/gun\n *\n * @deprecated Public gun relays drop frames often enough that a handshake\n * fails regularly. Kept until a third backend replaces it.\n */\nexport const gundb: SignalingProtocol = ({ topic, url }) => {\n const endpoint = url || \"wss://try.axe.eco/gun\";\n let connection: IGunInstance | undefined;\n\n return createSignalingLayer({\n type: \"gundb\",\n async setup() {\n log(\"GUNDB: Setting up\");\n const x = new (Gun as unknown as IGun)({\n peers: [endpoint],\n file: undefined,\n localStorage: undefined,\n });\n\n connection = x;\n\n await connection?.get(topic);\n },\n teardown() {\n connection?.get(topic).off();\n connection = undefined;\n },\n async publish(payload) {\n if (!connection) throw new SignalNoConnectionError();\n\n log(\"GUNDB: Publishing message\", topic, payload);\n connection.get(topic).put({ data: payload });\n },\n async subscribe(handler) {\n if (!connection) throw new SignalNoConnectionError();\n\n connection.get(topic).on((data) => {\n log(\"GUNDB: Received message\", data);\n\n const message = data.data?.toString();\n\n if (!message) return;\n\n handler(message);\n });\n },\n });\n};\n\nObject.assign(gundb, {\n __name: \"gundb\",\n});\n"]}
@@ -0,0 +1,12 @@
1
+ export declare const XR_PREFIX = "x";
2
+ export declare const XR_H_PREFIX = "h";
3
+ export type SignalingPrefix = typeof XR_PREFIX | typeof XR_H_PREFIX;
4
+ export type SignalingRecipient = "h" | "c";
5
+ export declare const HANDSHAKE_TIMEOUT_MS = 30000;
6
+ export declare const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
7
+ export declare const composeFrame: (prefix: SignalingPrefix, recipient: SignalingRecipient, ciphertext: string) => string;
8
+ export declare const parseFrame: (frame: string) => {
9
+ prefix: SignalingPrefix;
10
+ recipient: SignalingRecipient;
11
+ ciphertext: string;
12
+ } | undefined;
@@ -0,0 +1,15 @@
1
+ export const XR_PREFIX = "x";
2
+ export const XR_H_PREFIX = "h";
3
+ export const HANDSHAKE_TIMEOUT_MS = 30000;
4
+ export const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
5
+ export const composeFrame = (prefix, recipient, ciphertext) => prefix + recipient + ciphertext;
6
+ export const parseFrame = (frame) => {
7
+ const prefix = frame.slice(0, 1);
8
+ if (prefix !== XR_PREFIX && prefix !== XR_H_PREFIX)
9
+ return;
10
+ const recipient = frame.slice(1, 2);
11
+ if (recipient !== "h" && recipient !== "c")
12
+ return;
13
+ return { prefix, recipient, ciphertext: frame.slice(2) };
14
+ };
15
+ //# sourceMappingURL=handshake.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAC;AAI/B,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAM,CAAC;AAC3C,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC;AAEjD,MAAM,CAAC,MAAM,YAAY,GAAG,CAC1B,MAAuB,EACvB,SAA6B,EAC7B,UAAkB,EACV,EAAE,CAAC,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE7C,MAAM,CAAC,MAAM,UAAU,GAAG,CACxB,KAAa,EACgF,EAAE;IAC/F,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEjC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,WAAW;QAAE,OAAO;IAE3D,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAEpC,IAAI,SAAS,KAAK,GAAG,IAAI,SAAS,KAAK,GAAG;QAAE,OAAO;IAEnD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;AAC3D,CAAC,CAAC","sourcesContent":["export const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\nexport type SignalingPrefix = typeof XR_PREFIX | typeof XR_H_PREFIX;\nexport type SignalingRecipient = \"h\" | \"c\";\n\nexport const HANDSHAKE_TIMEOUT_MS = 30_000;\nexport const HANDSHAKE_RESEND_INTERVAL_MS = 2000;\n\nexport const composeFrame = (\n prefix: SignalingPrefix,\n recipient: SignalingRecipient,\n ciphertext: string,\n): string => prefix + recipient + ciphertext;\n\nexport const parseFrame = (\n frame: string,\n): { prefix: SignalingPrefix; recipient: SignalingRecipient; ciphertext: string; } | undefined => {\n const prefix = frame.slice(0, 1);\n\n if (prefix !== XR_PREFIX && prefix !== XR_H_PREFIX) return;\n\n const recipient = frame.slice(1, 2);\n\n if (recipient !== \"h\" && recipient !== \"c\") return;\n\n return { prefix, recipient, ciphertext: frame.slice(2) };\n};\n"]}
@@ -1,6 +1,7 @@
1
- import { deriveSymmetricKey, generateKeyPair, hashPublicKey, parseEncryptionKey, } 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
- import { createSignalingLayer, SIGNAL_STATE } from "./index.js";
3
+ import { createSignalingLayer, Status } from "./index.js";
4
+ const realSetTimeout = globalThis.setTimeout;
4
5
  /**
5
6
  * In-memory topic emulating a public relay: every publish is delivered to
6
7
  * all subscribers (including the sender, like MQTT echo). A `drop` predicate
@@ -9,9 +10,14 @@ import { createSignalingLayer, SIGNAL_STATE } from "./index.js";
9
10
  const neverDrop = () => false;
10
11
  const createTopic = () => {
11
12
  const subscribers = [];
13
+ const frames = [];
12
14
  let drop = neverDrop;
13
- let 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,62 +55,39 @@ const CLIENT_CAPABILITIES = {
46
55
  info: { identity: "com.example.wallet", name: "Example Wallet" },
47
56
  };
48
57
  const createPeer = async (channel, { isHost, h, k }) => {
49
- const { encryptionKey, decryptionKey } = await generateKeyPair();
50
- let relyingKey;
51
58
  let peerCapabilities;
52
59
  const layer = await createSignalingLayer(channel)({
53
60
  isHost,
54
61
  h,
55
- 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),
64
- rpDiscovered: async (rpKey) => {
65
- relyingKey = await parseEncryptionKey(rpKey);
66
- },
62
+ handshakeCipher: await createHandshakeCipher(k),
63
+ keyPair: generateKeyPair(),
67
64
  capabilities: CLIENT_CAPABILITIES,
68
- peerCapabilities: (capabilities) => {
69
- peerCapabilities = capabilities;
70
- },
71
65
  });
72
- return { layer, encryptionKey, getPeerCapabilities: () => peerCapabilities };
66
+ layer.peerCapabilities.subscribe(async (capabilities) => {
67
+ peerCapabilities = capabilities;
68
+ });
69
+ return { layer, getPeerCapabilities: () => peerCapabilities };
73
70
  };
74
71
  const K = "00112233445566778899aabbccddeeff";
75
72
  const setupPair = async (topic) => {
76
73
  // Mirror createSession: the host's h is the hash of its own key, the
77
74
  // client receives that h out-of-band (via the URI).
78
- const hostKeys = 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),
98
- rpDiscovered: async (rpKey) => {
99
- hostRelying = await parseEncryptionKey(rpKey);
100
- },
85
+ handshakeCipher: await createHandshakeCipher(K),
86
+ keyPair: hostKeys,
101
87
  capabilities: HOST_CAPABILITIES,
102
- peerCapabilities: (capabilities) => {
103
- hostPeerCapabilities = capabilities;
104
- },
88
+ });
89
+ host.peerCapabilities.subscribe((capabilities) => {
90
+ hostPeerCapabilities = capabilities;
105
91
  });
106
92
  const { layer: client, getPeerCapabilities: getClientPeerCapabilities } = await createPeer(topic.channel(), {
107
93
  isHost: false,
@@ -117,16 +103,37 @@ const setupPair = async (topic) => {
117
103
  };
118
104
  };
119
105
  const waitForState = (layer, target) => new Promise((resolve, reject) => {
120
- if (layer.getState().state === target)
106
+ if (layer.status.get() === target)
121
107
  return resolve();
122
- layer.on("state_change", (state) => {
108
+ layer.status.subscribe((state) => {
123
109
  if (state === target)
124
110
  resolve();
125
- if (state === SIGNAL_STATE.ERROR && target !== SIGNAL_STATE.ERROR) {
111
+ if (state === Status.ERROR && target !== Status.ERROR) {
112
+ // eslint-disable-next-line unicorn/no-multiple-promise-resolver-calls
126
113
  reject(new Error("signaling errored"));
127
114
  }
128
115
  });
129
116
  });
117
+ const bothEncrypted = (first, second) => Promise.all([
118
+ waitForState(first, Status.ENCRYPTED),
119
+ waitForState(second, Status.ENCRYPTED),
120
+ ]);
121
+ // The crypto behind each send finishes in real time, outside the fake clock
122
+ const advance = async (milliseconds) => {
123
+ await vi.advanceTimersByTimeAsync(milliseconds);
124
+ await new Promise(resolve => realSetTimeout(resolve, 50));
125
+ };
126
+ const recordHistory = (layer) => {
127
+ const statuses = [];
128
+ const peers = [];
129
+ layer.status.subscribe((value) => {
130
+ statuses.push(value);
131
+ });
132
+ layer.peer.subscribe((value) => {
133
+ peers.push(value);
134
+ });
135
+ return { statuses, peers };
136
+ };
130
137
  describe("signaling handshake (in-memory relay)", () => {
131
138
  let host;
132
139
  let client;
@@ -140,8 +147,8 @@ describe("signaling handshake (in-memory relay)", () => {
140
147
  ({ host, client } = pair);
141
148
  await host.setup();
142
149
  const encrypted = Promise.all([
143
- waitForState(host, SIGNAL_STATE.ENCRYPTED),
144
- waitForState(client, SIGNAL_STATE.ENCRYPTED),
150
+ waitForState(host, Status.ENCRYPTED),
151
+ waitForState(client, Status.ENCRYPTED),
145
152
  ]);
146
153
  await client.setup();
147
154
  await encrypted;
@@ -155,8 +162,8 @@ describe("signaling handshake (in-memory relay)", () => {
155
162
  ({ host, client } = pair);
156
163
  await host.setup();
157
164
  const encrypted = Promise.all([
158
- waitForState(host, SIGNAL_STATE.ENCRYPTED),
159
- waitForState(client, SIGNAL_STATE.ENCRYPTED),
165
+ waitForState(host, Status.ENCRYPTED),
166
+ waitForState(client, Status.ENCRYPTED),
160
167
  ]);
161
168
  await client.setup();
162
169
  await encrypted;
@@ -174,52 +181,182 @@ describe("signaling handshake (in-memory relay)", () => {
174
181
  topic.inject("hcQUJDRA==");
175
182
  topic.inject("xh!!!not-base64!!!");
176
183
  const encrypted = Promise.all([
177
- waitForState(host, SIGNAL_STATE.ENCRYPTED),
178
- waitForState(client, SIGNAL_STATE.ENCRYPTED),
184
+ waitForState(host, Status.ENCRYPTED),
185
+ waitForState(client, Status.ENCRYPTED),
179
186
  ]);
180
187
  await client.setup();
181
188
  topic.inject("hcgarbage-mid-handshake");
182
189
  await encrypted;
183
190
  });
184
- describe("lossy relay", () => {
191
+ describe("timeout", () => {
192
+ beforeEach(() => {
193
+ vi.useFakeTimers();
194
+ });
195
+ afterEach(() => {
196
+ vi.useRealTimers();
197
+ });
198
+ it("fails a joining peer whose host does not answer after 30 seconds", async () => {
199
+ const topic = createTopic();
200
+ topic.setDrop(() => true);
201
+ ({ host, client } = await setupPair(topic));
202
+ await host.setup();
203
+ await client.setup();
204
+ await vi.advanceTimersByTimeAsync(29999);
205
+ expect(client.status.get()).toBe(Status.HANDSHAKE);
206
+ await vi.advanceTimersByTimeAsync(1);
207
+ expect(client.status.get()).toBe(Status.ERROR);
208
+ });
209
+ it("fails a host 30 seconds after a flash when the joining peer goes quiet", async () => {
210
+ const topic = createTopic();
211
+ topic.setDrop(payload => !payload.startsWith("hh"));
212
+ ({ host, client } = await setupPair(topic));
213
+ await host.setup();
214
+ await client.setup();
215
+ await waitForState(host, Status.HANDSHAKE);
216
+ await vi.advanceTimersByTimeAsync(29999);
217
+ expect(host.status.get()).toBe(Status.HANDSHAKE);
218
+ await vi.advanceTimersByTimeAsync(1);
219
+ expect(host.status.get()).toBe(Status.ERROR);
220
+ });
221
+ it("keeps a completed handshake encrypted past the limit", async () => {
222
+ const topic = createTopic();
223
+ ({ host, client } = await setupPair(topic));
224
+ await host.setup();
225
+ const encrypted = Promise.all([
226
+ waitForState(host, Status.ENCRYPTED),
227
+ waitForState(client, Status.ENCRYPTED),
228
+ ]);
229
+ await client.setup();
230
+ await encrypted;
231
+ await vi.advanceTimersByTimeAsync(60000);
232
+ expect(host.status.get()).toBe(Status.ENCRYPTED);
233
+ expect(client.status.get()).toBe(Status.ENCRYPTED);
234
+ });
235
+ });
236
+ it("ignores duplicate deliveries of every frame", async () => {
237
+ const topic = createTopic();
238
+ topic.setCopies(2);
239
+ ({ host, client } = await setupPair(topic));
240
+ const hostHistory = recordHistory(host);
241
+ const clientHistory = recordHistory(client);
242
+ await host.setup();
243
+ const encrypted = bothEncrypted(host, client);
244
+ await client.setup();
245
+ await encrypted;
246
+ const path = ["standby", "connecting", "ready", "handshake", "handshake-partial", "encrypted"];
247
+ expect(hostHistory.statuses).toEqual(path);
248
+ expect(clientHistory.statuses).toEqual(path);
249
+ // The initial value and the one recorded peer
250
+ expect(hostHistory.peers).toHaveLength(2);
251
+ expect(clientHistory.peers).toHaveLength(2);
252
+ });
253
+ it("answers each duplicate capabilities of the host after the handshake", async () => {
254
+ const topic = createTopic();
255
+ ({ host, client } = await setupPair(topic));
256
+ await host.setup();
257
+ const encrypted = bothEncrypted(host, client);
258
+ await client.setup();
259
+ await encrypted;
260
+ const sent = topic.frames.length;
261
+ // "xc" is a peer-key frame for the joining peer: only the host's capabilities
262
+ const hostCapabilities = topic.frames.filter(frame => frame.startsWith("xc"));
263
+ for (const frame of [...hostCapabilities, ...hostCapabilities])
264
+ topic.inject(frame);
265
+ await vi.waitFor(() => expect(topic.frames).toHaveLength(sent + 2));
266
+ expect(topic.frames.slice(sent).every(frame => frame.startsWith("xh"))).toBe(true);
267
+ expect(host.status.get()).toBe(Status.ENCRYPTED);
268
+ expect(client.status.get()).toBe(Status.ENCRYPTED);
269
+ });
270
+ describe("re-send", () => {
185
271
  beforeEach(() => {
186
272
  vi.useFakeTimers();
187
273
  });
188
274
  afterEach(() => {
189
275
  vi.useRealTimers();
190
276
  });
191
- it("recovers when handshake frames are dropped", async () => {
277
+ it("sends a dropped flash again after 2 seconds", async () => {
278
+ const topic = createTopic();
279
+ topic.setDrop((_payload, index) => index === 0);
280
+ ({ host, client } = await setupPair(topic));
281
+ await host.setup();
282
+ await client.setup();
283
+ await advance(1999);
284
+ expect(host.status.get()).toBe(Status.READY);
285
+ await advance(1);
286
+ await bothEncrypted(host, client);
287
+ });
288
+ it("recovers when the first delivery of every frame is dropped", async () => {
192
289
  const topic = createTopic();
193
- // Drop the first delivery of every frame; re-sends get through.
194
290
  const seen = new Set();
195
291
  topic.setDrop((payload) => {
196
- if (seen.has(payload.slice(0, 2) + payload.length))
292
+ const key = payload.slice(0, 2) + payload.length;
293
+ if (seen.has(key))
197
294
  return false;
198
- seen.add(payload.slice(0, 2) + payload.length);
295
+ seen.add(key);
199
296
  return true;
200
297
  });
201
298
  ({ host, client } = await setupPair(topic));
202
299
  await host.setup();
203
300
  await client.setup();
204
- const encrypted = Promise.all([
205
- waitForState(host, SIGNAL_STATE.ENCRYPTED),
206
- waitForState(client, SIGNAL_STATE.ENCRYPTED),
207
- ]);
208
- // Let several resend intervals elapse.
301
+ const encrypted = bothEncrypted(host, client);
209
302
  for (let index = 0; index < 10; index += 1) {
210
- await vi.advanceTimersByTimeAsync(2100);
303
+ await advance(2100);
211
304
  }
212
305
  await encrypted;
213
306
  });
214
- 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 () => {
215
319
  const topic = createTopic();
216
320
  topic.setDrop(() => true);
217
321
  ({ host, client } = await setupPair(topic));
322
+ await host.setup();
323
+ await client.setup();
324
+ await advance(29999);
325
+ // The first flash and one re-send every 2 seconds from 2 s to 28 s
326
+ expect(topic.frames).toHaveLength(15);
327
+ await advance(1);
328
+ expect(client.status.get()).toBe(Status.ERROR);
329
+ await advance(60000);
330
+ expect(topic.frames).toHaveLength(15);
331
+ });
332
+ it("stops re-sending on teardown", async () => {
333
+ const topic = createTopic();
334
+ topic.setDrop(() => true);
335
+ ({ host, client } = await setupPair(topic));
336
+ await host.setup();
337
+ await client.setup();
338
+ await advance(5000);
339
+ await client.teardown();
340
+ const sent = topic.frames.length;
341
+ await advance(20000);
342
+ expect(topic.frames).toHaveLength(sent);
343
+ });
344
+ it("stops re-sending a step once the next one starts", async () => {
345
+ const topic = createTopic();
346
+ // The host's capabilities never arrive, so both peers stay in handshake-partial
347
+ topic.setDrop(payload => payload.startsWith("xc"));
348
+ ({ host, client } = await setupPair(topic));
349
+ await host.setup();
218
350
  await client.setup();
219
- const errored = waitForState(client, SIGNAL_STATE.ERROR);
220
- await vi.advanceTimersByTimeAsync(31000);
221
- await errored;
222
- expect(client.getState().state).toBe(SIGNAL_STATE.ERROR);
351
+ await Promise.all([host, client].map(layer => waitForState(layer, Status.HANDSHAKE_PARTIAL)));
352
+ const sent = topic.frames.length;
353
+ const sentSince = (prefix) => topic.frames.slice(sent).filter(frame => frame.startsWith(prefix));
354
+ await advance(10000);
355
+ // The flash and the host's pubkey stopped; the steps of handshake-partial still repeat
356
+ expect(sentSince("hh")).toEqual([]);
357
+ expect(sentSince("hc")).toEqual([]);
358
+ expect(sentSince("xh").length).toBeGreaterThan(1);
359
+ expect(sentSince("xc").length).toBeGreaterThan(1);
223
360
  });
224
361
  });
225
362
  });
@@ -1 +1 @@
1
- {"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../src/handshake.test.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n deriveSymmetricKey,\n type EncryptionKey,\n generateKeyPair,\n hashPublicKey,\n parseEncryptionKey,\n} from \"@openlv/core/encryption\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { createSignalingLayer, SIGNAL_STATE, type SignalingLayer } from \"./index.js\";\nimport type { PeerCapabilities } from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\n\n/**\n * In-memory topic emulating a public relay: every publish is delivered to\n * all subscribers (including the sender, like MQTT echo). A `drop` predicate\n * simulates lossy delivery.\n */\nconst neverDrop = () => false;\n\nconst createTopic = () => {\n const subscribers: ((payload: string) => void)[] = [];\n let drop: (payload: string, index: number) => boolean = neverDrop;\n let published = 0;\n\n return {\n setDrop(function_: (payload: string, index: number) => boolean) {\n drop = function_;\n },\n inject(payload: string) {\n for (const subscriber of subscribers) subscriber(payload);\n },\n channel(): SignalingChannel {\n return {\n type: \"memory\",\n setup: () => {},\n teardown: () => {},\n publish: (payload: string) => {\n const index = published++;\n\n if (drop(payload, index)) return;\n\n // Deliver asynchronously, as a real relay would.\n queueMicrotask(() => {\n for (const subscriber of subscribers) subscriber(payload);\n });\n },\n subscribe: (handler) => {\n subscribers.push(handler);\n },\n };\n },\n };\n};\n\nconst CLIENT_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\"],\n info: { identity: \"com.example.wallet\", name: \"Example Wallet\" },\n};\n\nconst createPeer = async (\n channel: SignalingChannel,\n { isHost, h, k }: { isHost: boolean; h: string; k: string; },\n) => {\n const { encryptionKey, decryptionKey } = await generateKeyPair();\n let relyingKey: EncryptionKey | undefined;\n let peerCapabilities: PeerCapabilities | undefined;\n\n const layer = await createSignalingLayer(channel)({\n isHost,\n h,\n k: await deriveSymmetricKey(k),\n publicKey: encryptionKey,\n canEncrypt: () => relyingKey !== undefined,\n encrypt: (message: string) => {\n if (!relyingKey) throw new Error(\"no peer key\");\n\n return relyingKey.encrypt(message);\n },\n decrypt: (message: string) => decryptionKey.decrypt(message),\n rpDiscovered: async (rpKey: string) => {\n relyingKey = await parseEncryptionKey(rpKey);\n },\n capabilities: CLIENT_CAPABILITIES,\n peerCapabilities: (capabilities) => {\n peerCapabilities = capabilities;\n },\n });\n\n return { layer, encryptionKey, getPeerCapabilities: () => peerCapabilities };\n};\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst setupPair = async (topic: ReturnType<typeof createTopic>) => {\n // Mirror createSession: the host's h is the hash of its own key, the\n // client receives that h out-of-band (via the URI).\n const hostKeys = await generateKeyPair();\n const h = await hashPublicKey(hostKeys.encryptionKey);\n\n const HOST_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\", \"ws\"],\n info: { identity: \"com.example.dapp\", name: \"Example dApp\" },\n };\n\n let hostRelying: EncryptionKey | undefined;\n let hostPeerCapabilities: PeerCapabilities | undefined;\n const host = await createSignalingLayer(topic.channel())({\n isHost: true,\n h,\n k: await deriveSymmetricKey(K),\n publicKey: hostKeys.encryptionKey,\n canEncrypt: () => hostRelying !== undefined,\n encrypt: (message: string) => {\n if (!hostRelying) throw new Error(\"no peer key\");\n\n return hostRelying.encrypt(message);\n },\n decrypt: (message: string) => hostKeys.decryptionKey.decrypt(message),\n rpDiscovered: async (rpKey: string) => {\n hostRelying = await parseEncryptionKey(rpKey);\n },\n capabilities: HOST_CAPABILITIES,\n peerCapabilities: (capabilities) => {\n hostPeerCapabilities = capabilities;\n },\n });\n\n const { layer: client, getPeerCapabilities: getClientPeerCapabilities }\n = await createPeer(topic.channel(), {\n isHost: false,\n h,\n k: K,\n });\n\n return {\n host,\n client,\n hostCapabilities: HOST_CAPABILITIES,\n getHostPeerCapabilities: () => hostPeerCapabilities,\n getClientPeerCapabilities,\n };\n};\n\nconst waitForState = (layer: SignalingLayer, target: string) =>\n new Promise<void>((resolve, reject) => {\n if (layer.getState().state === target) return resolve();\n\n layer.on(\"state_change\", (state) => {\n if (state === target) resolve();\n\n if (state === SIGNAL_STATE.ERROR && target !== SIGNAL_STATE.ERROR) {\n reject(new Error(\"signaling errored\"));\n }\n });\n });\n\ndescribe(\"signaling handshake (in-memory relay)\", () => {\n let host: SignalingLayer;\n let client: SignalingLayer;\n\n afterEach(async () => {\n await host?.teardown();\n await client?.teardown();\n });\n\n it(\"completes and exchanges data on a reliable relay\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n const received = new Promise<object>(resolve => host.on(\"message\", resolve));\n\n await client.send({ hello: \"world\" });\n expect(await received).toEqual({ hello: \"world\" });\n });\n\n it(\"exchanges capabilities in both directions during the handshake\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n expect(pair.getClientPeerCapabilities()).toEqual(pair.hostCapabilities);\n expect(pair.getHostPeerCapabilities()).toEqual(CLIENT_CAPABILITIES);\n });\n\n it(\"ignores garbage frames on the public topic\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n // Garbage before and during the handshake: random text, valid-looking\n // frames with undecryptable bodies, and truncated frames.\n topic.inject(\"not-a-frame\");\n topic.inject(\"hh\");\n topic.inject(\"hcQUJDRA==\");\n topic.inject(\"xh!!!not-base64!!!\");\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n await client.setup();\n topic.inject(\"hcgarbage-mid-handshake\");\n await encrypted;\n });\n\n describe(\"lossy relay\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"recovers when handshake frames are dropped\", async () => {\n const topic = createTopic();\n\n // Drop the first delivery of every frame; re-sends get through.\n const seen = new Set<string>();\n\n topic.setDrop((payload) => {\n if (seen.has(payload.slice(0, 2) + payload.length)) return false;\n\n seen.add(payload.slice(0, 2) + payload.length);\n\n return true;\n });\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n const encrypted = Promise.all([\n waitForState(host, SIGNAL_STATE.ENCRYPTED),\n waitForState(client, SIGNAL_STATE.ENCRYPTED),\n ]);\n\n // Let several resend intervals elapse.\n for (let index = 0; index < 10; index += 1) {\n await vi.advanceTimersByTimeAsync(2100);\n }\n\n await encrypted;\n });\n\n it(\"errors out when the peer never appears\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n\n ({ host, client } = await setupPair(topic));\n\n await client.setup();\n\n const errored = waitForState(client, SIGNAL_STATE.ERROR);\n\n await vi.advanceTimersByTimeAsync(31_000);\n await errored;\n expect(client.getState().state).toBe(SIGNAL_STATE.ERROR);\n });\n });\n});\n"]}
1
+ {"version":3,"file":"handshake.test.js","sourceRoot":"","sources":["../src/handshake.test.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,aAAa,GACd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAuB,MAAM,YAAY,CAAC;AAI/E,MAAM,cAAc,GAAG,UAAU,CAAC,UAAU,CAAC;AAE7C;;;;GAIG;AACH,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC;AAW9B,MAAM,WAAW,GAAG,GAAU,EAAE;IAC9B,MAAM,WAAW,GAAkC,EAAE,CAAC;IACtD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAgD,SAAS,CAAC;IAClE,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,OAAO;QACL,MAAM;QACN,SAAS,CAAC,KAAa;YACrB,MAAM,GAAG,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,CAAC,SAAsD;YAC5D,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;QACD,MAAM,CAAC,OAAe;YACpB,KAAK,MAAM,UAAU,IAAI,WAAW;gBAAE,UAAU,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO;YACL,OAAO;gBACL,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;gBACf,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC;gBAClB,OAAO,EAAE,CAAC,OAAe,EAAE,EAAE;oBAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC;oBAE5B,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAErB,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC;wBAAE,OAAO;oBAEjC,iDAAiD;oBACjD,cAAc,CAAC,GAAG,EAAE;wBAClB,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;4BACrC,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,EAAE,IAAI,IAAI,CAAC;gCAAE,UAAU,CAAC,OAAO,CAAC,CAAC;wBACnE,CAAC;oBACH,CAAC,CAAC,CAAC;gBACL,CAAC;gBACD,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;oBACrB,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC5B,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,mBAAmB,GAAqB;IAC5C,UAAU,EAAE,CAAC,MAAM,CAAC;IACpB,IAAI,EAAE,EAAE,QAAQ,EAAE,oBAAoB,EAAE,IAAI,EAAE,gBAAgB,EAAE;CACjE,CAAC;AAEF,MAAM,UAAU,GAAG,KAAK,EACtB,OAAyB,EACzB,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAA8C,EAC5D,EAAE;IACF,IAAI,gBAA8C,CAAC;IAEnD,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,CAAC;QAChD,MAAM;QACN,CAAC;QACD,eAAe,EAAE,MAAM,qBAAqB,CAAC,CAAC,CAAC;QAC/C,OAAO,EAAE,eAAe,EAAE;QAC1B,YAAY,EAAE,mBAAmB;KAClC,CAAC,CAAC;IAEH,KAAK,CAAC,gBAAgB,CAAC,SAAS,CAAC,KAAK,EAAE,YAAY,EAAE,EAAE;QACtD,gBAAgB,GAAG,YAAY,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC;AAChE,CAAC,CAAC;AAEF,MAAM,CAAC,GAAG,kCAAkC,CAAC;AAE7C,MAAM,SAAS,GAAG,KAAK,EAAE,KAAqC,EAAE,EAAE;IAChE,qEAAqE;IACrE,oDAAoD;IACpD,MAAM,QAAQ,GAAG,eAAe,EAAE,CAAC;IACnC,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IAElD,MAAM,iBAAiB,GAAqB;QAC1C,UAAU,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC;QAC1B,IAAI,EAAE,EAAE,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,cAAc,EAAE;KAC7D,CAAC;IAEF,IAAI,oBAAkD,CAAC;IACvD,MAAM,IAAI,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACvD,MAAM,EAAE,IAAI;QACZ,CAAC;QACD,eAAe,EAAE,MAAM,qBAAqB,CAAC,CAAC,CAAC;QAC/C,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,iBAAiB;KAChC,CAAC,CAAC;IAEH,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,YAAY,EAAE,EAAE;QAC/C,oBAAoB,GAAG,YAAY,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,GACnE,MAAM,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE;QAClC,MAAM,EAAE,KAAK;QACb,CAAC;QACD,CAAC,EAAE,CAAC;KACL,CAAC,CAAC;IAEL,OAAO;QACL,IAAI;QACJ,MAAM;QACN,gBAAgB,EAAE,iBAAiB;QACnC,uBAAuB,EAAE,GAAG,EAAE,CAAC,oBAAoB;QACnD,yBAAyB;KAC1B,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,KAAqB,EAAE,MAAc,EAAE,EAAE,CAC7D,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACpC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,MAAM;QAAE,OAAO,OAAO,EAAE,CAAC;IAEpD,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC/B,IAAI,KAAK,KAAK,MAAM;YAAE,OAAO,EAAE,CAAC;QAEhC,IAAI,KAAK,KAAK,MAAM,CAAC,KAAK,IAAI,MAAM,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACtD,sEAAsE;YACtE,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,MAAM,aAAa,GAAG,CAAC,KAAqB,EAAE,MAAsB,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC;IACnF,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,CAAC;IACrC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;CACvC,CAAC,CAAC;AAEH,4EAA4E;AAC5E,MAAM,OAAO,GAAG,KAAK,EAAE,YAAoB,EAAE,EAAE;IAC7C,MAAM,EAAE,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAC;IAChD,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC5D,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,CAAC,KAAqB,EAAE,EAAE;IAC9C,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAc,EAAE,CAAC;IAE5B,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC/B,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAC7B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC,CAAC;AAEF,QAAQ,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACrD,IAAI,IAAoB,CAAC;IACzB,IAAI,MAAsB,CAAC;IAE3B,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,IAAI,EAAE,QAAQ,EAAE,CAAC;QACvB,MAAM,MAAM,EAAE,QAAQ,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QAEpC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;QAE1B,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAEnB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;YAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC;YACpC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,SAAS,CAAC;QAEhB,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAS,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QAE7E,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC;QAEpC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;QAE1B,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAEnB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;YAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC;YACpC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,SAAS,CAAC;QAEhB,MAAM,CAAC,IAAI,CAAC,yBAAyB,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACxE,MAAM,CAAC,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAE5B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAEnB,sEAAsE;QACtE,0DAA0D;QAC1D,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC5B,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACnB,KAAK,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC3B,KAAK,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QAEnC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;YAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC;YACpC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;SACvC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,KAAK,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC;QACxC,MAAM,SAAS,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,GAAG,EAAE;YACb,EAAE,CAAC,aAAa,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;YAChF,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YAErB,MAAM,EAAE,CAAC,wBAAwB,CAAC,KAAM,CAAC,CAAC;YAC1C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAEnD,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;YACtF,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YACpD,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAE3C,MAAM,EAAE,CAAC,wBAAwB,CAAC,KAAM,CAAC,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAEjD,MAAM,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YAEnB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC;gBAC5B,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,CAAC;gBACpC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,SAAS,CAAC;aACvC,CAAC,CAAC;YAEH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,SAAS,CAAC;YAEhB,MAAM,EAAE,CAAC,wBAAwB,CAAC,KAAM,CAAC,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACjD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;QAC3D,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAE5B,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAE5C,MAAM,WAAW,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,aAAa,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAEnB,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAE9C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,SAAS,CAAC;QAEhB,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,WAAW,CAAC,CAAC;QAE/F,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7C,8CAA8C;QAC9C,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAE5B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;QAEnB,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAE9C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,SAAS,CAAC;QAEhB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;QACjC,8EAA8E;QAC9E,MAAM,gBAAgB,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAE9E,KAAK,MAAM,KAAK,IAAI,CAAC,GAAG,gBAAgB,EAAE,GAAG,gBAAgB,CAAC;YAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEpF,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC;QAEpE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;QACvB,UAAU,CAAC,GAAG,EAAE;YACd,EAAE,CAAC,aAAa,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,SAAS,CAAC,GAAG,EAAE;YACb,EAAE,CAAC,aAAa,EAAE,CAAC;QACrB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,6CAA6C,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,KAAK,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;YAChD,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YAErB,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAE7C,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;YACjB,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;YAC1E,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAE/B,KAAK,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBACxB,MAAM,GAAG,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;gBAEjD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;oBAAE,OAAO,KAAK,CAAC;gBAEhC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAEd,OAAO,IAAI,CAAC;YACd,CAAC,CAAC,CAAC;YAEH,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YAErB,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAE9C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;gBAC3C,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAED,MAAM,SAAS,CAAC;QAClB,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;YAC/C,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YAEnB,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAE9C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,SAAS,CAAC;YAEhB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAEjC,MAAM,OAAO,CAAC,KAAM,CAAC,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YAErB,MAAM,OAAO,CAAC,KAAM,CAAC,CAAC;YACtB,mEAAmE;YACnE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;YAEtC,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;YACjB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAE/C,MAAM,OAAO,CAAC,KAAM,CAAC,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;QACxC,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;YAC5C,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;YAExB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YAEjC,MAAM,OAAO,CAAC,KAAM,CAAC,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;YAChE,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;YAE5B,gFAAgF;YAChF,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YACnD,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;YAE5C,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;YAE9F,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;YACjC,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;YAEzG,MAAM,OAAO,CAAC,KAAM,CAAC,CAAC;YAEtB,uFAAuF;YACvF,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACpC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAClD,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import {\n createHandshakeCipher,\n generateKeyPair,\n hashPublicKey,\n} from \"@openlv/core/encryption\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { createSignalingLayer, Status, type SignalingLayer } from \"./index.js\";\nimport type { PeerCapabilities } from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\n\nconst realSetTimeout = globalThis.setTimeout;\n\n/**\n * In-memory topic emulating a public relay: every publish is delivered to\n * all subscribers (including the sender, like MQTT echo). A `drop` predicate\n * simulates lossy delivery.\n */\nconst neverDrop = () => false;\n\ntype Topic = {\n setDrop: (function_: (payload: string, index: number) => boolean) => void;\n setCopies: (copies: number) => void;\n inject: (payload: string) => void;\n channel: () => SignalingChannel;\n // Every published payload, including the dropped ones\n frames: string[];\n};\n\nconst createTopic = (): Topic => {\n const subscribers: ((payload: string) => void)[] = [];\n const frames: string[] = [];\n let drop: (payload: string, index: number) => boolean = neverDrop;\n let copies = 1;\n\n return {\n frames,\n setCopies(count: number) {\n copies = count;\n },\n setDrop(function_: (payload: string, index: number) => boolean) {\n drop = function_;\n },\n inject(payload: string) {\n for (const subscriber of subscribers) subscriber(payload);\n },\n channel(): SignalingChannel {\n return {\n type: \"memory\",\n setup: () => {},\n teardown: () => {},\n publish: (payload: string) => {\n const index = frames.length;\n\n frames.push(payload);\n\n if (drop(payload, index)) return;\n\n // Deliver asynchronously, as a real relay would.\n queueMicrotask(() => {\n for (const subscriber of subscribers) {\n for (let copy = 0; copy < copies; copy += 1) subscriber(payload);\n }\n });\n },\n subscribe: (handler) => {\n subscribers.push(handler);\n },\n };\n },\n };\n};\n\nconst CLIENT_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\"],\n info: { identity: \"com.example.wallet\", name: \"Example Wallet\" },\n};\n\nconst createPeer = async (\n channel: SignalingChannel,\n { isHost, h, k }: { isHost: boolean; h: string; k: string; },\n) => {\n let peerCapabilities: PeerCapabilities | undefined;\n\n const layer = await createSignalingLayer(channel)({\n isHost,\n h,\n handshakeCipher: await createHandshakeCipher(k),\n keyPair: generateKeyPair(),\n capabilities: CLIENT_CAPABILITIES,\n });\n\n layer.peerCapabilities.subscribe(async (capabilities) => {\n peerCapabilities = capabilities;\n });\n\n return { layer, getPeerCapabilities: () => peerCapabilities };\n};\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst setupPair = async (topic: ReturnType<typeof createTopic>) => {\n // Mirror createSession: the host's h is the hash of its own key, the\n // client receives that h out-of-band (via the URI).\n const hostKeys = generateKeyPair();\n const h = await hashPublicKey(hostKeys.publicKey);\n\n const HOST_CAPABILITIES: PeerCapabilities = {\n transports: [\"wrtc\", \"ws\"],\n info: { identity: \"com.example.dapp\", name: \"Example dApp\" },\n };\n\n let hostPeerCapabilities: PeerCapabilities | undefined;\n const host = await createSignalingLayer(topic.channel())({\n isHost: true,\n h,\n handshakeCipher: await createHandshakeCipher(K),\n keyPair: hostKeys,\n capabilities: HOST_CAPABILITIES,\n });\n\n host.peerCapabilities.subscribe((capabilities) => {\n hostPeerCapabilities = capabilities;\n });\n\n const { layer: client, getPeerCapabilities: getClientPeerCapabilities }\n = await createPeer(topic.channel(), {\n isHost: false,\n h,\n k: K,\n });\n\n return {\n host,\n client,\n hostCapabilities: HOST_CAPABILITIES,\n getHostPeerCapabilities: () => hostPeerCapabilities,\n getClientPeerCapabilities,\n };\n};\n\nconst waitForState = (layer: SignalingLayer, target: string) =>\n new Promise<void>((resolve, reject) => {\n if (layer.status.get() === target) return resolve();\n\n layer.status.subscribe((state) => {\n if (state === target) resolve();\n\n if (state === Status.ERROR && target !== Status.ERROR) {\n // eslint-disable-next-line unicorn/no-multiple-promise-resolver-calls\n reject(new Error(\"signaling errored\"));\n }\n });\n });\n\nconst bothEncrypted = (first: SignalingLayer, second: SignalingLayer) => Promise.all([\n waitForState(first, Status.ENCRYPTED),\n waitForState(second, Status.ENCRYPTED),\n]);\n\n// The crypto behind each send finishes in real time, outside the fake clock\nconst advance = async (milliseconds: number) => {\n await vi.advanceTimersByTimeAsync(milliseconds);\n await new Promise(resolve => realSetTimeout(resolve, 50));\n};\n\nconst recordHistory = (layer: SignalingLayer) => {\n const statuses: string[] = [];\n const peers: unknown[] = [];\n\n layer.status.subscribe((value) => {\n statuses.push(value);\n });\n layer.peer.subscribe((value) => {\n peers.push(value);\n });\n\n return { statuses, peers };\n};\n\ndescribe(\"signaling handshake (in-memory relay)\", () => {\n let host: SignalingLayer;\n let client: SignalingLayer;\n\n afterEach(async () => {\n await host?.teardown();\n await client?.teardown();\n });\n\n it(\"completes and exchanges data on a reliable relay\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n const received = new Promise<object>(resolve => host.on(\"message\", resolve));\n\n await client.send({ hello: \"world\" });\n expect(await received).toEqual({ hello: \"world\" });\n });\n\n it(\"exchanges capabilities in both directions during the handshake\", async () => {\n const topic = createTopic();\n const pair = await setupPair(topic);\n\n ({ host, client } = pair);\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n expect(pair.getClientPeerCapabilities()).toEqual(pair.hostCapabilities);\n expect(pair.getHostPeerCapabilities()).toEqual(CLIENT_CAPABILITIES);\n });\n\n it(\"ignores garbage frames on the public topic\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n // Garbage before and during the handshake: random text, valid-looking\n // frames with undecryptable bodies, and truncated frames.\n topic.inject(\"not-a-frame\");\n topic.inject(\"hh\");\n topic.inject(\"hcQUJDRA==\");\n topic.inject(\"xh!!!not-base64!!!\");\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n topic.inject(\"hcgarbage-mid-handshake\");\n await encrypted;\n });\n\n describe(\"timeout\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"fails a joining peer whose host does not answer after 30 seconds\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await vi.advanceTimersByTimeAsync(29_999);\n expect(client.status.get()).toBe(Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(1);\n expect(client.status.get()).toBe(Status.ERROR);\n });\n\n it(\"fails a host 30 seconds after a flash when the joining peer goes quiet\", async () => {\n const topic = createTopic();\n\n topic.setDrop(payload => !payload.startsWith(\"hh\"));\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await waitForState(host, Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(29_999);\n expect(host.status.get()).toBe(Status.HANDSHAKE);\n\n await vi.advanceTimersByTimeAsync(1);\n expect(host.status.get()).toBe(Status.ERROR);\n });\n\n it(\"keeps a completed handshake encrypted past the limit\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = Promise.all([\n waitForState(host, Status.ENCRYPTED),\n waitForState(client, Status.ENCRYPTED),\n ]);\n\n await client.setup();\n await encrypted;\n\n await vi.advanceTimersByTimeAsync(60_000);\n expect(host.status.get()).toBe(Status.ENCRYPTED);\n expect(client.status.get()).toBe(Status.ENCRYPTED);\n });\n });\n\n it(\"ignores duplicate deliveries of every frame\", async () => {\n const topic = createTopic();\n\n topic.setCopies(2);\n ({ host, client } = await setupPair(topic));\n\n const hostHistory = recordHistory(host);\n const clientHistory = recordHistory(client);\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const path = [\"standby\", \"connecting\", \"ready\", \"handshake\", \"handshake-partial\", \"encrypted\"];\n\n expect(hostHistory.statuses).toEqual(path);\n expect(clientHistory.statuses).toEqual(path);\n // The initial value and the one recorded peer\n expect(hostHistory.peers).toHaveLength(2);\n expect(clientHistory.peers).toHaveLength(2);\n });\n\n it(\"answers each duplicate capabilities of the host after the handshake\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const sent = topic.frames.length;\n // \"xc\" is a peer-key frame for the joining peer: only the host's capabilities\n const hostCapabilities = topic.frames.filter(frame => frame.startsWith(\"xc\"));\n\n for (const frame of [...hostCapabilities, ...hostCapabilities]) topic.inject(frame);\n\n await vi.waitFor(() => expect(topic.frames).toHaveLength(sent + 2));\n\n expect(topic.frames.slice(sent).every(frame => frame.startsWith(\"xh\"))).toBe(true);\n expect(host.status.get()).toBe(Status.ENCRYPTED);\n expect(client.status.get()).toBe(Status.ENCRYPTED);\n });\n\n describe(\"re-send\", () => {\n beforeEach(() => {\n vi.useFakeTimers();\n });\n\n afterEach(() => {\n vi.useRealTimers();\n });\n\n it(\"sends a dropped flash again after 2 seconds\", async () => {\n const topic = createTopic();\n\n topic.setDrop((_payload, index) => index === 0);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await advance(1999);\n expect(host.status.get()).toBe(Status.READY);\n\n await advance(1);\n await bothEncrypted(host, client);\n });\n\n it(\"recovers when the first delivery of every frame is dropped\", async () => {\n const topic = createTopic();\n const seen = new Set<string>();\n\n topic.setDrop((payload) => {\n const key = payload.slice(0, 2) + payload.length;\n\n if (seen.has(key)) return false;\n\n seen.add(key);\n\n return true;\n });\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n for (let index = 0; index < 10; index += 1) {\n await advance(2100);\n }\n\n await encrypted;\n });\n\n it(\"stops re-sending once encrypted\", async () => {\n const topic = createTopic();\n\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n\n const encrypted = bothEncrypted(host, client);\n\n await client.setup();\n await encrypted;\n\n const sent = topic.frames.length;\n\n await advance(60_000);\n expect(topic.frames).toHaveLength(sent);\n });\n\n it(\"re-sends every 2 seconds until the limit, then stops\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n\n await advance(29_999);\n // The first flash and one re-send every 2 seconds from 2 s to 28 s\n expect(topic.frames).toHaveLength(15);\n\n await advance(1);\n expect(client.status.get()).toBe(Status.ERROR);\n\n await advance(60_000);\n expect(topic.frames).toHaveLength(15);\n });\n\n it(\"stops re-sending on teardown\", async () => {\n const topic = createTopic();\n\n topic.setDrop(() => true);\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await advance(5000);\n await client.teardown();\n\n const sent = topic.frames.length;\n\n await advance(20_000);\n expect(topic.frames).toHaveLength(sent);\n });\n\n it(\"stops re-sending a step once the next one starts\", async () => {\n const topic = createTopic();\n\n // The host's capabilities never arrive, so both peers stay in handshake-partial\n topic.setDrop(payload => payload.startsWith(\"xc\"));\n ({ host, client } = await setupPair(topic));\n\n await host.setup();\n await client.setup();\n await Promise.all([host, client].map(layer => waitForState(layer, Status.HANDSHAKE_PARTIAL)));\n\n const sent = topic.frames.length;\n const sentSince = (prefix: string) => topic.frames.slice(sent).filter(frame => frame.startsWith(prefix));\n\n await advance(10_000);\n\n // The flash and the host's pubkey stopped; the steps of handshake-partial still repeat\n expect(sentSince(\"hh\")).toEqual([]);\n expect(sentSince(\"hc\")).toEqual([]);\n expect(sentSince(\"xh\").length).toBeGreaterThan(1);\n expect(sentSince(\"xc\").length).toBeGreaterThan(1);\n });\n });\n});\n"]}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import type { EncryptionKey, SymmetricKey } from "@openlv/core/encryption";
1
+ import { type Observable } from "@openlv/core";
2
+ import type { Cipher, KeyPair, PublicKey } from "@openlv/core/encryption";
2
3
  import { EventEmitter } from "eventemitter3";
3
- import type { MaybePromise } from "viem";
4
4
  import { type PeerCapabilities } from "./messages.js";
5
5
  import type { SignalingChannel } from "./protocol.js";
6
6
  export * from "./messages.js";
7
7
  export * from "./protocol.js";
8
- export declare const SIGNAL_STATE: {
8
+ export declare const Status: {
9
9
  readonly STANDBY: "standby";
10
10
  readonly CONNECTING: "connecting";
11
11
  readonly READY: "ready";
@@ -14,39 +14,36 @@ export declare const SIGNAL_STATE: {
14
14
  readonly ENCRYPTED: "encrypted";
15
15
  readonly ERROR: "error";
16
16
  };
17
- export type SignalState = (typeof SIGNAL_STATE)[keyof typeof SIGNAL_STATE];
17
+ export type Status = (typeof Status)[keyof typeof Status];
18
18
  export type SignalEventMap = {
19
- state_change: (state: SignalState) => void;
20
19
  message: (message: object) => void;
21
20
  };
22
- export type SignalingProperties = {
21
+ export type SignalingHooks = {
23
22
  isHost: boolean;
24
23
  h: string;
25
- k?: SymmetricKey;
26
- rpDiscovered: (rpKey: string) => MaybePromise<void>;
24
+ handshakeCipher: Cipher;
25
+ keyPair: KeyPair;
27
26
  capabilities: PeerCapabilities;
28
- peerCapabilities: (capabilities: PeerCapabilities) => MaybePromise<void>;
29
- decrypt: (message: string) => MaybePromise<string>;
30
- encrypt: (message: string) => MaybePromise<string>;
31
- publicKey: EncryptionKey;
32
- canEncrypt: () => boolean;
27
+ };
28
+ export type Peer = {
29
+ peerKey: PublicKey;
30
+ cipher: Cipher;
33
31
  };
34
32
  export type SignalingContext = {
35
33
  type: string;
36
- send: (message: object) => MaybePromise<void>;
37
- setup: () => MaybePromise<void>;
38
- teardown: () => MaybePromise<void>;
39
- getState: () => {
40
- state: SignalState;
41
- };
34
+ send: (message: object) => Promise<void>;
35
+ setup: () => Promise<void>;
36
+ teardown: () => Promise<void>;
37
+ status: Observable<Status>;
38
+ peer: Observable<Peer | undefined>;
39
+ peerCapabilities: Observable<PeerCapabilities | undefined>;
42
40
  };
43
41
  export type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;
44
- export type SignalingLayerFunction = (properties: SignalingProperties) => Promise<SignalingLayer>;
45
- export declare const XR_PREFIX = "x";
46
- export declare const XR_H_PREFIX = "h";
42
+ export type SignalingLayerFunction = (hooks: SignalingHooks) => Promise<SignalingLayer>;
43
+ export type CreateSignalingLayerFunction = (base: SignalingChannel) => SignalingLayerFunction;
47
44
  /**
48
45
  * Base Signaling Layer implementation
49
46
  *
50
47
  * https://openlv.sh/api/signaling
51
48
  */
52
- export declare const createSignalingLayer: (init: SignalingChannel) => SignalingLayerFunction;
49
+ export declare const createSignalingLayer: CreateSignalingLayerFunction;