@openlv/signaling 0.1.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/gundb/index.d.ts +3 -0
- package/dist/gundb/index.js +3 -0
- package/dist/gundb/index.js.map +1 -1
- package/dist/handshake.d.ts +12 -0
- package/dist/handshake.js +15 -0
- package/dist/handshake.js.map +1 -0
- package/dist/handshake.test.js +204 -67
- package/dist/handshake.test.js.map +1 -1
- package/dist/index.d.ts +20 -23
- package/dist/index.js +92 -200
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +22 -19
- package/dist/index.test.js.map +1 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.js +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/messages.test.js +1 -1
- package/dist/messages.test.js.map +1 -1
- package/dist/mqtt/index.js +4 -2
- package/dist/mqtt/index.js.map +1 -1
- package/package.json +2 -7
- package/dist/dynamic.d.ts +0 -6
- package/dist/dynamic.js +0 -23
- package/dist/dynamic.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { make } from "@openlv/core";
|
|
2
|
-
import {
|
|
1
|
+
import { createScope, make, observable } from "@openlv/core";
|
|
2
|
+
import { acceptPeer, createPeerCipher, hashPublicKey, parsePublicKey, toBase64, } from "@openlv/core/encryption";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
4
|
import { match } from "ts-pattern";
|
|
5
|
+
import { composeFrame, HANDSHAKE_RESEND_INTERVAL_MS, HANDSHAKE_TIMEOUT_MS, parseFrame, XR_H_PREFIX, XR_PREFIX, } from "./handshake.js";
|
|
5
6
|
import { parseSignalMessage, } from "./messages.js";
|
|
6
7
|
import { log } from "./utils/log.js";
|
|
7
8
|
export * from "./messages.js";
|
|
8
9
|
export * from "./protocol.js";
|
|
9
|
-
export const
|
|
10
|
+
export const Status = {
|
|
10
11
|
STANDBY: "standby",
|
|
11
12
|
CONNECTING: "connecting",
|
|
12
13
|
READY: "ready",
|
|
@@ -15,106 +16,36 @@ export const SIGNAL_STATE = {
|
|
|
15
16
|
ENCRYPTED: "encrypted",
|
|
16
17
|
ERROR: "error",
|
|
17
18
|
};
|
|
18
|
-
export const XR_PREFIX = "x";
|
|
19
|
-
export const XR_H_PREFIX = "h";
|
|
20
|
-
/**
|
|
21
|
-
* Relays are lossy (MQTT QoS 0, ntfy best-effort), so every handshake step
|
|
22
|
-
* is re-sent on an interval until the state machine observes progress.
|
|
23
|
-
* Receivers treat duplicates as no-ops, which keeps re-sends wire-compatible.
|
|
24
|
-
*/
|
|
25
|
-
const HANDSHAKE_RESEND_INTERVAL_MS = 2000;
|
|
26
|
-
const HANDSHAKE_TIMEOUT_MS = 30000;
|
|
27
19
|
/**
|
|
28
20
|
* Base Signaling Layer implementation
|
|
29
21
|
*
|
|
30
22
|
* https://openlv.sh/api/signaling
|
|
31
23
|
*/
|
|
32
|
-
export const createSignalingLayer =
|
|
24
|
+
export const createSignalingLayer = channel => async (hooks) => {
|
|
25
|
+
const { capabilities, h, handshakeCipher, keyPair, isHost, } = hooks;
|
|
26
|
+
const [status, setStatus] = observable(Status.STANDBY);
|
|
27
|
+
const [peer, setPeer] = observable(undefined);
|
|
28
|
+
const [peerCapabilities, setPeerCapabilities] = observable(undefined);
|
|
33
29
|
const emitter = new EventEmitter();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const stopTimers = () => {
|
|
45
|
-
stopResend();
|
|
46
|
-
clearTimeout(deadlineTimer);
|
|
47
|
-
deadlineTimer = undefined;
|
|
48
|
-
};
|
|
49
|
-
const setState = (_state) => {
|
|
50
|
-
if (state === _state)
|
|
51
|
-
return;
|
|
52
|
-
state = _state;
|
|
53
|
-
if (_state === SIGNAL_STATE.ENCRYPTED || _state === SIGNAL_STATE.ERROR) {
|
|
54
|
-
stopTimers();
|
|
55
|
-
}
|
|
56
|
-
emitter.emit("state_change", _state);
|
|
57
|
-
};
|
|
58
|
-
const send = async (method, recipient, payload) => {
|
|
59
|
-
const prefix = match(method)
|
|
60
|
-
.with("handshake", () => XR_H_PREFIX)
|
|
61
|
-
.with("encrypted", () => XR_PREFIX)
|
|
62
|
-
.exhaustive();
|
|
63
|
-
const message = await match(method)
|
|
64
|
-
.with("handshake", () => {
|
|
65
|
-
if (!handshakeKey)
|
|
66
|
-
return;
|
|
67
|
-
return handshakeKey.encrypt(JSON.stringify(payload));
|
|
68
|
-
})
|
|
69
|
-
.with("encrypted", () => {
|
|
70
|
-
if (!canEncrypt())
|
|
71
|
-
return;
|
|
72
|
-
return encrypt(JSON.stringify(payload));
|
|
73
|
-
})
|
|
74
|
-
.exhaustive();
|
|
75
|
-
if (message === undefined) {
|
|
76
|
-
throw new Error(`Cannot encrypt ${method} frame: key not available`);
|
|
77
|
-
}
|
|
78
|
-
await init.publish(prefix + recipient + message);
|
|
79
|
-
};
|
|
80
|
-
/**
|
|
81
|
-
* Send a handshake step now, then keep re-sending it until the next step
|
|
82
|
-
* (or teardown) supersedes it. Errors are logged and swallowed — the next
|
|
83
|
-
* tick retries.
|
|
84
|
-
*/
|
|
85
|
-
const sendRepeating = (method, recipient, payload) => {
|
|
86
|
-
stopResend();
|
|
87
|
-
const attempt = () => send(method, recipient, payload)
|
|
88
|
-
.catch(error => log("handshake send failed, will retry", error));
|
|
89
|
-
resendTimer = setInterval(attempt, HANDSHAKE_RESEND_INTERVAL_MS);
|
|
90
|
-
return attempt();
|
|
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));
|
|
91
40
|
};
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
41
|
+
const enterStep = (from, next, prefix, message, nextPeer = peer.get()) => {
|
|
42
|
+
if (status.get() !== from)
|
|
94
43
|
return;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}, HANDSHAKE_TIMEOUT_MS);
|
|
101
|
-
};
|
|
102
|
-
const recordPeerKey = async (key) => {
|
|
103
|
-
// Never overwrite an established peer key — a second, different pubkey
|
|
104
|
-
// mid-handshake is either a duplicate delivery or an injection attempt.
|
|
105
|
-
if (isPeerKeyRecorded)
|
|
106
|
-
return false;
|
|
107
|
-
isPeerKeyRecorded = true;
|
|
108
|
-
await rpDiscovered(key);
|
|
109
|
-
return true;
|
|
110
|
-
};
|
|
111
|
-
const recordPeerCapabilities = async (peerCaps) => {
|
|
112
|
-
// Same overwrite guard as the peer key: only the first delivery counts,
|
|
113
|
-
// re-sent duplicates are no-ops.
|
|
114
|
-
if (isPeerCapabilitiesRecorded)
|
|
115
|
-
return;
|
|
116
|
-
isPeerCapabilitiesRecorded = true;
|
|
117
|
-
await peerCapabilities(peerCaps);
|
|
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));
|
|
118
49
|
};
|
|
119
50
|
const capabilitiesMessage = () => ({
|
|
120
51
|
type: "capabilities",
|
|
@@ -123,142 +54,103 @@ export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decr
|
|
|
123
54
|
});
|
|
124
55
|
const pubkeyMessage = () => ({
|
|
125
56
|
type: "pubkey",
|
|
126
|
-
payload: { publicKey: publicKey
|
|
57
|
+
payload: { publicKey: toBase64(keyPair.publicKey) },
|
|
127
58
|
timestamp: Date.now(),
|
|
128
59
|
});
|
|
129
60
|
const handleHandshakeFrame = async (message) => {
|
|
130
|
-
await match({ msg: message,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
setState(SIGNAL_STATE.HANDSHAKE);
|
|
135
|
-
startHandshakeDeadline();
|
|
136
|
-
await sendRepeating("handshake", "c", pubkeyMessage());
|
|
61
|
+
await match({ msg: message, status: status.get(), isHost })
|
|
62
|
+
.with({ msg: { type: "flash" }, status: Status.READY, isHost: true }, async () => {
|
|
63
|
+
handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));
|
|
64
|
+
enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, pubkeyMessage());
|
|
137
65
|
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
setState(SIGNAL_STATE.ERROR);
|
|
144
|
-
log("Received host public key does not match expected hash — possible tampering");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
setState(SIGNAL_STATE.ERROR);
|
|
150
|
-
log("Failed to parse received host public key");
|
|
66
|
+
.with({ msg: { type: "pubkey" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {
|
|
67
|
+
const peerKey = parsePublicKey(messagePayload.publicKey);
|
|
68
|
+
if (!peerKey || await hashPublicKey(peerKey) !== h) {
|
|
69
|
+
setStatus(Status.ERROR);
|
|
70
|
+
log("Received host public key is malformed or does not match expected hash -- possible tampering");
|
|
151
71
|
return;
|
|
152
72
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
|
|
156
|
-
return await sendRepeating("encrypted", "h", pubkeyMessage());
|
|
73
|
+
const cipher = await createPeerCipher(keyPair, peerKey);
|
|
74
|
+
enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, pubkeyMessage(), { peerKey, cipher });
|
|
157
75
|
})
|
|
158
76
|
.otherwise(() => {
|
|
159
|
-
log("Ignoring handshake frame", message.type, "in
|
|
77
|
+
log("Ignoring handshake frame", message.type, "in status", status.get());
|
|
160
78
|
});
|
|
161
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
|
+
};
|
|
162
96
|
const handleEncryptedFrame = async (message) => {
|
|
163
|
-
await match({ msg: message,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);
|
|
169
|
-
return await sendRepeating("encrypted", "c", capabilitiesMessage());
|
|
170
|
-
})
|
|
171
|
-
.with({ msg: { type: "capabilities" }, state: SIGNAL_STATE.HANDSHAKE_PARTIAL }, async ({ msg: { payload: messagePayload } }) => {
|
|
172
|
-
await recordPeerCapabilities(messagePayload);
|
|
173
|
-
setState(SIGNAL_STATE.ENCRYPTED);
|
|
97
|
+
await match({ msg: message, status: status.get(), isHost })
|
|
98
|
+
.with({ msg: { type: "capabilities" }, status: Status.HANDSHAKE_PARTIAL }, async ({ msg: { payload: messagePayload } }) => {
|
|
99
|
+
await setPeerCapabilities(messagePayload);
|
|
100
|
+
setStatus(Status.ENCRYPTED);
|
|
174
101
|
if (isHost)
|
|
175
102
|
return;
|
|
176
|
-
return await
|
|
103
|
+
return await publish(XR_PREFIX, capabilitiesMessage());
|
|
177
104
|
})
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// can finish.
|
|
181
|
-
.with({ msg: { type: "capabilities" }, state: SIGNAL_STATE.ENCRYPTED, isHost: false }, async () => await send("encrypted", "h", capabilitiesMessage()))
|
|
182
|
-
.with({ msg: { type: "data" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {
|
|
105
|
+
.with({ msg: { type: "capabilities" }, status: Status.ENCRYPTED, isHost: false }, async () => await publish(XR_PREFIX, capabilitiesMessage()))
|
|
106
|
+
.with({ msg: { type: "data" }, status: Status.ENCRYPTED }, async () => {
|
|
183
107
|
emitter.emit("message", message.payload);
|
|
184
108
|
})
|
|
185
109
|
.otherwise(() => {
|
|
186
|
-
log("Ignoring encrypted frame", message.type, "in
|
|
110
|
+
log("Ignoring encrypted frame", message.type, "in status", status.get());
|
|
187
111
|
});
|
|
188
112
|
};
|
|
189
|
-
const
|
|
190
|
-
// The topic is public: anyone can publish garbage. Nothing in this
|
|
191
|
-
// handler may throw past this boundary, otherwise a single malformed
|
|
192
|
-
// frame becomes an unhandled rejection.
|
|
113
|
+
const onReceive = async (frame) => {
|
|
193
114
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const body = payload.slice(2);
|
|
197
|
-
const isRecipient = (isHost ? "h" : "c") === recipient;
|
|
198
|
-
if (!isRecipient)
|
|
115
|
+
const parsed = parseFrame(frame);
|
|
116
|
+
if (parsed?.recipient !== (isHost ? "h" : "c"))
|
|
199
117
|
return;
|
|
118
|
+
const { prefix, ciphertext } = parsed;
|
|
200
119
|
if (prefix === XR_H_PREFIX) {
|
|
201
|
-
|
|
202
|
-
return;
|
|
203
|
-
const message = parseSignalMessage(await handshakeKey.decrypt(body));
|
|
120
|
+
const message = parseSignalMessage(await handshakeCipher.decrypt(ciphertext));
|
|
204
121
|
if (message)
|
|
205
122
|
await handleHandshakeFrame(message);
|
|
123
|
+
return;
|
|
206
124
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
log("Dropping frame with unknown prefix");
|
|
214
|
-
}
|
|
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);
|
|
215
131
|
}
|
|
216
132
|
catch (error) {
|
|
217
133
|
log("Dropping undecryptable or malformed frame", error);
|
|
218
134
|
}
|
|
219
135
|
};
|
|
136
|
+
const setup = async () => {
|
|
137
|
+
setStatus(Status.CONNECTING);
|
|
138
|
+
await channel.setup();
|
|
139
|
+
await channel.subscribe(onReceive);
|
|
140
|
+
setStatus(Status.READY);
|
|
141
|
+
if (!isHost) {
|
|
142
|
+
handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));
|
|
143
|
+
enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, { type: "flash", payload: {}, timestamp: Date.now() });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
220
146
|
return make(emitter, {
|
|
221
|
-
type:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
setState(SIGNAL_STATE.READY);
|
|
231
|
-
if (!isHost) {
|
|
232
|
-
// Enter HANDSHAKE before publishing: the host's pubkey reply can
|
|
233
|
-
// arrive while the publish is still in flight.
|
|
234
|
-
setState(SIGNAL_STATE.HANDSHAKE);
|
|
235
|
-
startHandshakeDeadline();
|
|
236
|
-
await sendRepeating("handshake", "h", {
|
|
237
|
-
type: "flash",
|
|
238
|
-
payload: {},
|
|
239
|
-
timestamp: Date.now(),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
async teardown() {
|
|
244
|
-
log("teardown");
|
|
245
|
-
stopTimers();
|
|
246
|
-
await init.teardown?.();
|
|
247
|
-
},
|
|
248
|
-
send(message) {
|
|
249
|
-
if (!canEncrypt()) {
|
|
250
|
-
return Promise.reject(new Error("Cannot encrypt message before keys are exchanged"));
|
|
251
|
-
}
|
|
252
|
-
const them = isHost ? "c" : "h";
|
|
253
|
-
return send("encrypted", them, {
|
|
254
|
-
type: "data",
|
|
255
|
-
payload: message,
|
|
256
|
-
timestamp: Date.now(),
|
|
257
|
-
});
|
|
258
|
-
},
|
|
259
|
-
getState() {
|
|
260
|
-
return { state: state };
|
|
261
|
-
},
|
|
147
|
+
type: channel.type,
|
|
148
|
+
setup,
|
|
149
|
+
teardown: () => scope.close(),
|
|
150
|
+
send: (message) => publish(XR_PREFIX, { type: "data", payload: message, timestamp: Date.now() }),
|
|
151
|
+
status,
|
|
152
|
+
peer,
|
|
153
|
+
peerCapabilities,
|
|
262
154
|
});
|
|
263
155
|
};
|
|
264
156
|
//# sourceMappingURL=index.js.map
|
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 } 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\";\nimport type { MaybePromise } from \"viem\";\n\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 SIGNAL_STATE = {\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 SignalState = (typeof SIGNAL_STATE)[keyof typeof SIGNAL_STATE];\n\nexport type SignalEventMap = {\n state_change: (state: SignalState) => void;\n message: (message: object) => void;\n};\n\nexport type SignalingProperties = {\n isHost: boolean;\n h: string;\n k?: SymmetricKey;\n rpDiscovered: (rpKey: string) => MaybePromise<void>;\n // Our capabilities, advertised to the peer during the handshake\n capabilities: PeerCapabilities;\n peerCapabilities: (capabilities: PeerCapabilities) => MaybePromise<void>;\n // Decrypt using our private key\n decrypt: (message: string) => MaybePromise<string>;\n // Encrypt to relying party\n encrypt: (message: string) => MaybePromise<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) => MaybePromise<void>;\n setup: () => MaybePromise<void>;\n teardown: () => MaybePromise<void>;\n\n getState: () => {\n state: SignalState;\n };\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFunction = (\n properties: SignalingProperties,\n) => Promise<SignalingLayer>;\n\nexport const XR_PREFIX = \"x\";\nexport const XR_H_PREFIX = \"h\";\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 */\nconst HANDSHAKE_RESEND_INTERVAL_MS = 2000;\nconst HANDSHAKE_TIMEOUT_MS = 30_000;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer = (\n init: SignalingChannel,\n): SignalingLayerFunction => async ({\n canEncrypt,\n encrypt,\n decrypt,\n rpDiscovered,\n capabilities,\n peerCapabilities,\n h,\n k,\n publicKey,\n isHost,\n}: SignalingProperties) => {\n const emitter = new EventEmitter<SignalEventMap>();\n let state: SignalState = SIGNAL_STATE.STANDBY;\n let isPeerKeyRecorded = false;\n let isPeerCapabilitiesRecorded = false;\n let resendTimer: ReturnType<typeof setInterval> | undefined;\n let deadlineTimer: ReturnType<typeof setTimeout> | undefined;\n const handshakeKey = k || undefined;\n\n const stopResend = () => {\n clearInterval(resendTimer);\n resendTimer = undefined;\n };\n const stopTimers = () => {\n stopResend();\n clearTimeout(deadlineTimer);\n deadlineTimer = undefined;\n };\n\n const setState = (_state: SignalState) => {\n if (state === _state) return;\n\n state = _state;\n\n if (_state === SIGNAL_STATE.ENCRYPTED || _state === SIGNAL_STATE.ERROR) {\n stopTimers();\n }\n\n emitter.emit(\"state_change\", _state);\n };\n\n const send = async (\n method: \"handshake\" | \"encrypted\",\n recipient: \"h\" | \"c\",\n payload: SignalMessage,\n ) => {\n const prefix = match(method)\n .with(\"handshake\", () => XR_H_PREFIX)\n .with(\"encrypted\", () => XR_PREFIX)\n .exhaustive();\n\n const message = await match(method)\n .with(\"handshake\", () => {\n if (!handshakeKey) return;\n\n return handshakeKey.encrypt(JSON.stringify(payload));\n })\n .with(\"encrypted\", () => {\n if (!canEncrypt()) return;\n\n return encrypt(JSON.stringify(payload));\n })\n .exhaustive();\n\n if (message === undefined) {\n throw new Error(`Cannot encrypt ${method} frame: key not available`);\n }\n\n await init.publish(prefix + recipient + message);\n };\n\n /**\n * Send a handshake step now, then keep re-sending it until the next step\n * (or teardown) supersedes it. Errors are logged and swallowed — the next\n * tick retries.\n */\n const sendRepeating = (\n method: \"handshake\" | \"encrypted\",\n recipient: \"h\" | \"c\",\n payload: SignalMessage,\n ) => {\n stopResend();\n\n const attempt = () => send(method, recipient, payload)\n .catch(error => log(\"handshake send failed, will retry\", error));\n\n resendTimer = setInterval(attempt, HANDSHAKE_RESEND_INTERVAL_MS);\n\n return attempt();\n };\n\n const startHandshakeDeadline = () => {\n if (deadlineTimer) return;\n\n deadlineTimer = setTimeout(() => {\n if (state === SIGNAL_STATE.ENCRYPTED) return;\n\n log(\"handshake timed out\");\n setState(SIGNAL_STATE.ERROR);\n }, HANDSHAKE_TIMEOUT_MS);\n };\n\n const recordPeerKey = async (key: string): Promise<boolean> => {\n // Never overwrite an established peer key — a second, different pubkey\n // mid-handshake is either a duplicate delivery or an injection attempt.\n if (isPeerKeyRecorded) return false;\n\n isPeerKeyRecorded = true;\n await rpDiscovered(key);\n\n return true;\n };\n\n const recordPeerCapabilities = async (\n peerCaps: PeerCapabilities,\n ): Promise<void> => {\n // Same overwrite guard as the peer key: only the first delivery counts,\n // re-sent duplicates are no-ops.\n if (isPeerCapabilitiesRecorded) return;\n\n isPeerCapabilitiesRecorded = true;\n await peerCapabilities(peerCaps);\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: publicKey.toString() },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (message: SignalMessage) => {\n await match({ msg: message, state, isHost })\n // Host: a client wants to connect. Re-entered on duplicate `flash`\n // deliveries (client re-sends until it hears our pubkey).\n .with({ msg: { type: \"flash\" }, state: SIGNAL_STATE.READY, isHost: true }, async () => {\n setState(SIGNAL_STATE.HANDSHAKE);\n startHandshakeDeadline();\n await sendRepeating(\"handshake\", \"c\", pubkeyMessage());\n })\n // Client: host announced its public key.\n .with({ msg: { type: \"pubkey\" }, isHost: false, state: SIGNAL_STATE.HANDSHAKE }, async ({ msg: { payload: messagePayload } }) => {\n try {\n const receivedKey = await parseEncryptionKey(messagePayload.publicKey);\n\n if (!await validatePublicKeyHash(receivedKey, h)) {\n setState(SIGNAL_STATE.ERROR);\n log(\"Received host public key does not match expected hash — possible tampering\");\n\n return;\n }\n }\n catch {\n setState(SIGNAL_STATE.ERROR);\n log(\"Failed to parse received host public key\");\n\n return;\n }\n\n if (!await recordPeerKey(messagePayload.publicKey)) return;\n\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await sendRepeating(\"encrypted\", \"h\", pubkeyMessage());\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", message.type, \"in state\", state);\n });\n };\n\n const handleEncryptedFrame = async (message: SignalMessage) => {\n await match({ msg: message, state, isHost })\n // Host: client responded with its public key.\n .with(\n { msg: { type: \"pubkey\" }, isHost: true, state: SIGNAL_STATE.HANDSHAKE },\n async ({ msg: { payload: messagePayload } }) => {\n if (!await recordPeerKey(messagePayload.publicKey)) return;\n\n setState(SIGNAL_STATE.HANDSHAKE_PARTIAL);\n\n return await sendRepeating(\"encrypted\", \"c\", capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, state: SIGNAL_STATE.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: messagePayload } }) => {\n await recordPeerCapabilities(messagePayload);\n setState(SIGNAL_STATE.ENCRYPTED);\n\n if (isHost) return;\n\n return await send(\"encrypted\", \"h\", 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\" }, state: SIGNAL_STATE.ENCRYPTED, isHost: false },\n async () => await send(\"encrypted\", \"h\", capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, state: SIGNAL_STATE.ENCRYPTED }, async () => {\n emitter.emit(\"message\", message.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", message.type, \"in state\", state);\n });\n };\n\n const handleReceive = async (payload: string) => {\n // The topic is public: anyone can publish garbage. Nothing in this\n // handler may throw past this boundary, otherwise a single malformed\n // frame becomes an unhandled rejection.\n try {\n const prefix = payload.slice(0, 1);\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 if (prefix === XR_H_PREFIX) {\n if (!handshakeKey) return;\n\n const message = parseSignalMessage(await handshakeKey.decrypt(body));\n\n if (message) await handleHandshakeFrame(message);\n }\n else if (prefix === XR_PREFIX) {\n const message = parseSignalMessage(await decrypt(body));\n\n if (message) await handleEncryptedFrame(message);\n }\n else {\n log(\"Dropping frame with unknown prefix\");\n }\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n return make(emitter, {\n type: init.type,\n async setup() {\n setState(SIGNAL_STATE.CONNECTING);\n await init.setup();\n await init.subscribe(handleReceive);\n\n if (canEncrypt()) {\n setState(SIGNAL_STATE.ENCRYPTED);\n\n return;\n }\n\n setState(SIGNAL_STATE.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 setState(SIGNAL_STATE.HANDSHAKE);\n startHandshakeDeadline();\n await sendRepeating(\"handshake\", \"h\", {\n type: \"flash\",\n payload: {},\n timestamp: Date.now(),\n });\n }\n },\n async teardown() {\n log(\"teardown\");\n stopTimers();\n await init.teardown?.();\n },\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 const them = isHost ? \"c\" : \"h\";\n\n return send(\"encrypted\", them, {\n type: \"data\",\n payload: message,\n timestamp: Date.now(),\n });\n },\n getState() {\n return { state: state };\n },\n });\n};\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { createScope, make, type Observable, observable } from \"@openlv/core\";\nimport type { AcceptedPeer, Cipher, KeyPair, PublicKey } from \"@openlv/core/encryption\";\nimport {\n acceptPeer,\n createPeerCipher,\n hashPublicKey,\n parsePublicKey,\n toBase64,\n} from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport {\n composeFrame,\n HANDSHAKE_RESEND_INTERVAL_MS,\n HANDSHAKE_TIMEOUT_MS,\n parseFrame,\n type SignalingPrefix,\n XR_H_PREFIX,\n XR_PREFIX,\n} from \"./handshake.js\";\nimport {\n parseSignalMessage,\n type PeerCapabilities,\n type SignalMessage,\n} from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./messages.js\";\nexport * from \"./protocol.js\";\n\nexport const Status = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type Status = (typeof Status)[keyof typeof Status];\n\nexport type SignalEventMap = {\n message: (message: object) => void;\n};\n\nexport type SignalingHooks = {\n isHost: boolean;\n h: string;\n handshakeCipher: Cipher;\n keyPair: KeyPair;\n capabilities: PeerCapabilities;\n};\n\nexport type Peer = {\n peerKey: PublicKey;\n cipher: Cipher;\n};\n\nexport type SignalingContext = {\n type: string;\n\n send: (message: object) => Promise<void>;\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n\n status: Observable<Status>;\n peer: Observable<Peer | undefined>;\n peerCapabilities: Observable<PeerCapabilities | undefined>;\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFunction = (\n hooks: SignalingHooks,\n) => Promise<SignalingLayer>;\n\nexport type CreateSignalingLayerFunction = (\n base: SignalingChannel,\n) => SignalingLayerFunction;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer: CreateSignalingLayerFunction = channel => async (hooks: SignalingHooks) => {\n const {\n capabilities,\n h,\n handshakeCipher,\n keyPair,\n isHost,\n } = hooks;\n\n const [status, setStatus] = observable<Status>(Status.STANDBY);\n const [peer, setPeer] = observable<Peer | undefined>(undefined);\n const [peerCapabilities, setPeerCapabilities] = observable<PeerCapabilities | undefined>(undefined);\n\n const emitter = new EventEmitter<SignalEventMap>();\n const scope = createScope();\n\n scope.add(channel.teardown);\n\n const handshake = scope.child();\n\n handshake.closeWhen(status, value => value === Status.ENCRYPTED || value === Status.ERROR);\n\n const publish = async (prefix: SignalingPrefix, message: SignalMessage) => {\n const cipher = prefix === XR_H_PREFIX ? handshakeCipher : peer.get()?.cipher;\n\n if (!cipher) throw new Error(\"Cannot encrypt message before keys are exchanged\");\n\n const ciphertext = await cipher.encrypt(JSON.stringify(message));\n\n await channel.publish(composeFrame(prefix, isHost ? \"c\" : \"h\", ciphertext));\n };\n\n const enterStep = (from: Status, next: Status, prefix: SignalingPrefix, message: SignalMessage, nextPeer = peer.get()) => {\n if (status.get() !== from) return;\n\n setPeer(nextPeer);\n setStatus(next);\n\n const step = handshake.child();\n\n step.closeWhen(status, value => value !== next);\n step.repeat(HANDSHAKE_RESEND_INTERVAL_MS, () => publish(prefix, message));\n };\n\n const capabilitiesMessage = (): SignalMessage => ({\n type: \"capabilities\",\n payload: capabilities,\n timestamp: Date.now(),\n });\n const pubkeyMessage = (): SignalMessage => ({\n type: \"pubkey\",\n payload: { publicKey: toBase64(keyPair.publicKey) },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with({ msg: { type: \"flash\" }, status: Status.READY, isHost: true }, async () => {\n handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));\n enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, pubkeyMessage());\n })\n .with({ msg: { type: \"pubkey\" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {\n const peerKey = parsePublicKey(messagePayload.publicKey);\n\n if (!peerKey || await hashPublicKey(peerKey) !== h) {\n setStatus(Status.ERROR);\n log(\"Received host public key is malformed or does not match expected hash -- possible tampering\");\n\n return;\n }\n\n const cipher = await createPeerCipher(keyPair, peerKey);\n\n enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, pubkeyMessage(), { peerKey, cipher });\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", message.type, \"in status\", status.get());\n });\n };\n\n /**\n * The host has no peer key yet when the joining peer's `pubkey` arrives, so\n * the key in the frame is the only one it can check the announced key against.\n */\n const handleAcceptedPeer = async ({ peerKey, cipher, message }: AcceptedPeer) => {\n const signalMessage = parseSignalMessage(message);\n\n if (!isHost || status.get() !== Status.HANDSHAKE || signalMessage?.type !== \"pubkey\") {\n log(\"Ignoring first peer-key frame in status\", status.get());\n\n return;\n }\n\n if (signalMessage.payload.publicKey !== toBase64(peerKey)) {\n log(\"Ignoring pubkey that differs from the frame key\");\n\n return;\n }\n\n enterStep(Status.HANDSHAKE, Status.HANDSHAKE_PARTIAL, XR_PREFIX, capabilitiesMessage(), { peerKey, cipher });\n };\n\n const handleEncryptedFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with(\n { msg: { type: \"capabilities\" }, status: Status.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: messagePayload } }) => {\n await setPeerCapabilities(messagePayload);\n setStatus(Status.ENCRYPTED);\n\n if (isHost) return;\n\n return await publish(XR_PREFIX, capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, status: Status.ENCRYPTED, isHost: false },\n async () => await publish(XR_PREFIX, capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, status: Status.ENCRYPTED }, async () => {\n emitter.emit(\"message\", message.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", message.type, \"in status\", status.get());\n });\n };\n\n const onReceive = async (frame: string) => {\n try {\n const parsed = parseFrame(frame);\n\n if (parsed?.recipient !== (isHost ? \"h\" : \"c\")) return;\n\n const { prefix, ciphertext } = parsed;\n\n if (prefix === XR_H_PREFIX) {\n const message = parseSignalMessage(await handshakeCipher.decrypt(ciphertext));\n\n if (message) await handleHandshakeFrame(message);\n\n return;\n }\n\n const current = peer.get();\n\n if (!current) return await handleAcceptedPeer(await acceptPeer(keyPair, ciphertext));\n\n const message = parseSignalMessage(await current.cipher.decrypt(ciphertext));\n\n if (message) await handleEncryptedFrame(message);\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n const setup = async () => {\n setStatus(Status.CONNECTING);\n await channel.setup();\n await channel.subscribe(onReceive);\n\n setStatus(Status.READY);\n\n if (!isHost) {\n handshake.timeout(HANDSHAKE_TIMEOUT_MS, () => setStatus(Status.ERROR));\n enterStep(Status.READY, Status.HANDSHAKE, XR_H_PREFIX, { type: \"flash\", payload: {}, timestamp: Date.now() });\n }\n };\n\n return make(emitter, {\n type: channel.type,\n setup,\n teardown: () => scope.close(),\n send: (message: object) => publish(XR_PREFIX, { type: \"data\", payload: message, timestamp: Date.now() }),\n status,\n peer,\n peerCapabilities,\n });\n};\n"]}
|
package/dist/index.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { 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
|
|
7
|
+
const K = "00112233445566778899aabbccddeeff";
|
|
7
8
|
const providersByType = [
|
|
8
9
|
[
|
|
9
10
|
"mqtt",
|
|
@@ -29,7 +30,7 @@ const providersByType = [
|
|
|
29
30
|
mqtt,
|
|
30
31
|
{
|
|
31
32
|
topic: "mytesttopic1111",
|
|
32
|
-
url: "
|
|
33
|
+
url: "wss://test.mosquitto.org:8081/mqtt",
|
|
33
34
|
},
|
|
34
35
|
],
|
|
35
36
|
[
|
|
@@ -57,7 +58,7 @@ const providersByType = [
|
|
|
57
58
|
],
|
|
58
59
|
],
|
|
59
60
|
],
|
|
60
|
-
//
|
|
61
|
+
// gundb is deprecated and its public relays are too lossy to assert on.
|
|
61
62
|
// [
|
|
62
63
|
// "gundb",
|
|
63
64
|
// [
|
|
@@ -70,34 +71,36 @@ const providersByType = [
|
|
|
70
71
|
// ],
|
|
71
72
|
];
|
|
72
73
|
const testSignalingLayer = async (layer, properties) => {
|
|
73
|
-
const
|
|
74
|
-
const h =
|
|
74
|
+
const hostKeys = generateKeyPair();
|
|
75
|
+
const h = await hashPublicKey(hostKeys.publicKey);
|
|
76
|
+
const handshakeCipher = await createHandshakeCipher(K);
|
|
75
77
|
const signalingLayer = await layer(properties);
|
|
76
78
|
const signalA = await signalingLayer({
|
|
77
79
|
h,
|
|
78
|
-
|
|
80
|
+
handshakeCipher,
|
|
79
81
|
capabilities: { transports: ["wrtc"] },
|
|
80
|
-
|
|
81
|
-
canEncrypt: () => true,
|
|
82
|
-
encrypt: publicKey.encrypt,
|
|
83
|
-
decrypt: decryptionKey.decrypt,
|
|
84
|
-
publicKey,
|
|
82
|
+
keyPair: hostKeys,
|
|
85
83
|
isHost: true,
|
|
86
84
|
});
|
|
85
|
+
signalA.peer.subscribe(v => log("rpKey", v?.peerKey));
|
|
86
|
+
signalA.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
87
87
|
const signalB = await signalingLayer({
|
|
88
88
|
h,
|
|
89
|
-
|
|
89
|
+
handshakeCipher,
|
|
90
90
|
capabilities: { transports: ["wrtc"] },
|
|
91
|
-
|
|
92
|
-
canEncrypt: () => true,
|
|
93
|
-
encrypt: publicKey.encrypt,
|
|
94
|
-
decrypt: decryptionKey.decrypt,
|
|
95
|
-
publicKey,
|
|
91
|
+
keyPair: generateKeyPair(),
|
|
96
92
|
isHost: false,
|
|
97
93
|
});
|
|
94
|
+
signalB.peer.subscribe(v => log("rpKey", v?.peerKey));
|
|
95
|
+
signalB.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
98
96
|
expect(signalA).toBeDefined();
|
|
99
97
|
expect(signalB).toBeDefined();
|
|
100
|
-
await
|
|
98
|
+
await signalA.setup();
|
|
99
|
+
await signalB.setup();
|
|
100
|
+
await Promise.all([
|
|
101
|
+
signalA.status.until(state => state === Status.ENCRYPTED),
|
|
102
|
+
signalB.status.until(state => state === Status.ENCRYPTED),
|
|
103
|
+
]);
|
|
101
104
|
const messageReceivedA = new Promise((resolve) => {
|
|
102
105
|
signalA.on("message", (message) => {
|
|
103
106
|
log("messageReceivedA", message);
|
package/dist/index.test.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { 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\";\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 // TODO: GunDB is disabled\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\n const signalingLayer = await layer(properties);\n const signalA = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n capabilities: { transports: [\"wrtc\"] },\n peerCapabilities: v => log(\"peerCapabilities\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: true,\n });\n const signalB = await signalingLayer({\n h,\n rpDiscovered: v => log(\"rpKey\", v),\n capabilities: { transports: [\"wrtc\"] },\n peerCapabilities: v => log(\"peerCapabilities\", v),\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: false,\n });\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await Promise.all([signalA.setup(), signalB.setup()]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult\n = | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, properties), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: properties.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: properties.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, properties]) => testProvider(layer, properties)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHandshakeCipher, generateKeyPair, hashPublicKey } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { Status } from \"./index.js\";\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://test.mosquitto.org:8081/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // gundb is deprecated and its public relays are too lossy to assert on.\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<void> => {\n const hostKeys = generateKeyPair();\n const h = await hashPublicKey(hostKeys.publicKey);\n const handshakeCipher = await createHandshakeCipher(K);\n\n const signalingLayer = await layer(properties);\n const signalA = await signalingLayer({\n h,\n handshakeCipher,\n capabilities: { transports: [\"wrtc\"] },\n keyPair: hostKeys,\n isHost: true,\n });\n\n signalA.peer.subscribe(v => log(\"rpKey\", v?.peerKey));\n signalA.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n const signalB = await signalingLayer({\n h,\n handshakeCipher,\n capabilities: { transports: [\"wrtc\"] },\n keyPair: generateKeyPair(),\n isHost: false,\n });\n\n signalB.peer.subscribe(v => log(\"rpKey\", v?.peerKey));\n signalB.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await signalA.setup();\n await signalB.setup();\n await Promise.all([\n signalA.status.until(state => state === Status.ENCRYPTED),\n signalB.status.until(state => state === Status.ENCRYPTED),\n ]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult\n = | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, properties), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: properties.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: properties.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, properties]) => testProvider(layer, properties)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
|
package/dist/messages.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type PeerInfo = {
|
|
|
17
17
|
identity: string;
|
|
18
18
|
name: string;
|
|
19
19
|
/**
|
|
20
|
-
* A data URI or an image URL. Only size is enforced on the wire
|
|
20
|
+
* A data URI or an image URL. Only size is enforced on the wire -- anyone
|
|
21
21
|
* rendering or fetching it is responsible for sanity-checking it first.
|
|
22
22
|
*/
|
|
23
23
|
icon?: string;
|
package/dist/messages.js
CHANGED
|
@@ -20,7 +20,7 @@ export const validatePeerInfo = (info) => {
|
|
|
20
20
|
return `info.name must be 1-${MAX_TEXT_LENGTH} characters`;
|
|
21
21
|
}
|
|
22
22
|
if (info.icon !== undefined && !isBoundedString(info.icon, MAX_ICON_LENGTH)) {
|
|
23
|
-
return `info.icon must be 1-${MAX_ICON_LENGTH} characters
|
|
23
|
+
return `info.icon must be 1-${MAX_ICON_LENGTH} characters -- use a smaller image or an image URL`;
|
|
24
24
|
}
|
|
25
25
|
return undefined;
|
|
26
26
|
};
|