@openlv/signaling 0.1.2 → 0.2.0
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 +20 -0
- package/dist/handshake.js +38 -0
- package/dist/handshake.js.map +1 -0
- package/dist/handshake.test.js +32 -30
- package/dist/handshake.test.js.map +1 -1
- package/dist/index.d.ts +16 -20
- package/dist/index.js +87 -186
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +17 -13
- package/dist/index.test.js.map +1 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.js +13 -13
- 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 +5 -1
- package/dist/mqtt/index.js.map +1 -1
- package/dist/ntfy/url.js +1 -1
- package/dist/ntfy/url.js.map +1 -1
- package/dist/ntfy/url.test.js +4 -4
- package/dist/ntfy/url.test.js.map +1 -1
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js.map +1 -1
- package/dist/utils/log.d.ts +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";
|
|
1
|
+
import { make, observable } from "@openlv/core";
|
|
2
2
|
import { parseEncryptionKey, validatePublicKeyHash } from "@openlv/core/encryption";
|
|
3
3
|
import { EventEmitter } from "eventemitter3";
|
|
4
4
|
import { match } from "ts-pattern";
|
|
5
|
-
import {
|
|
5
|
+
import { handshake, XR_H_PREFIX, XR_PREFIX } from "./handshake.js";
|
|
6
|
+
import {} 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,107 +16,24 @@ 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 { canEncrypt, encrypt, decrypt, capabilities, h, k: handshakeKey, publicKey, isHost, } = hooks;
|
|
26
|
+
const [status, setStatus] = observable(Status.STANDBY);
|
|
27
|
+
const [peerKey, setPeerKey] = observable(undefined);
|
|
28
|
+
const [peerCapabilities, setPeerCapabilities] = observable(undefined);
|
|
33
29
|
const emitter = new EventEmitter();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
clearInterval(resendTimer);
|
|
42
|
-
resendTimer = undefined;
|
|
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();
|
|
91
|
-
};
|
|
92
|
-
const startHandshakeDeadline = () => {
|
|
93
|
-
if (deadlineTimer)
|
|
94
|
-
return;
|
|
95
|
-
deadlineTimer = setTimeout(() => {
|
|
96
|
-
if (state === SIGNAL_STATE.ENCRYPTED)
|
|
97
|
-
return;
|
|
98
|
-
log("handshake timed out");
|
|
99
|
-
setState(SIGNAL_STATE.ERROR);
|
|
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 (peerKeyRecorded)
|
|
106
|
-
return false;
|
|
107
|
-
peerKeyRecorded = 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 (peerCapabilitiesRecorded)
|
|
115
|
-
return;
|
|
116
|
-
peerCapabilitiesRecorded = true;
|
|
117
|
-
await peerCapabilities(peerCaps);
|
|
118
|
-
};
|
|
30
|
+
const { frame, parse } = handshake({
|
|
31
|
+
isHost,
|
|
32
|
+
handshakeKey,
|
|
33
|
+
encrypt,
|
|
34
|
+
decrypt,
|
|
35
|
+
});
|
|
36
|
+
const send = async (method, payload) => await channel.publish(await frame(method, payload));
|
|
119
37
|
const capabilitiesMessage = () => ({
|
|
120
38
|
type: "capabilities",
|
|
121
39
|
payload: capabilities,
|
|
@@ -126,139 +44,122 @@ export const createSignalingLayer = (init) => async ({ canEncrypt, encrypt, decr
|
|
|
126
44
|
payload: { publicKey: publicKey.toString() },
|
|
127
45
|
timestamp: Date.now(),
|
|
128
46
|
});
|
|
129
|
-
const handleHandshakeFrame = async (
|
|
130
|
-
await match({ msg,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
setState(SIGNAL_STATE.HANDSHAKE);
|
|
135
|
-
startHandshakeDeadline();
|
|
136
|
-
await sendRepeating("handshake", "c", pubkeyMessage());
|
|
47
|
+
const handleHandshakeFrame = async (message) => {
|
|
48
|
+
await match({ msg: message, status: status.get(), isHost })
|
|
49
|
+
.with({ msg: { type: "flash" }, status: Status.READY, isHost: true }, async () => {
|
|
50
|
+
setStatus(Status.HANDSHAKE);
|
|
51
|
+
await send("handshake", pubkeyMessage());
|
|
137
52
|
})
|
|
138
|
-
|
|
139
|
-
|
|
53
|
+
.with({ msg: { type: "pubkey" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {
|
|
54
|
+
let receivedKey;
|
|
140
55
|
try {
|
|
141
|
-
|
|
56
|
+
receivedKey = await parseEncryptionKey(messagePayload.publicKey);
|
|
142
57
|
if (!await validatePublicKeyHash(receivedKey, h)) {
|
|
143
|
-
|
|
144
|
-
log("Received host public key does not match expected hash
|
|
58
|
+
setStatus(Status.ERROR);
|
|
59
|
+
log("Received host public key does not match expected hash -- possible tampering");
|
|
145
60
|
return;
|
|
146
61
|
}
|
|
147
62
|
}
|
|
148
63
|
catch {
|
|
149
|
-
|
|
64
|
+
setStatus(Status.ERROR);
|
|
150
65
|
log("Failed to parse received host public key");
|
|
151
66
|
return;
|
|
152
67
|
}
|
|
153
|
-
|
|
68
|
+
const changed = setPeerKey(receivedKey)
|
|
69
|
+
&& setStatus(Status.HANDSHAKE_PARTIAL);
|
|
70
|
+
if (!changed)
|
|
154
71
|
return;
|
|
155
|
-
|
|
156
|
-
return await sendRepeating("encrypted", "h", pubkeyMessage());
|
|
72
|
+
return await send("encrypted", pubkeyMessage());
|
|
157
73
|
})
|
|
158
74
|
.otherwise(() => {
|
|
159
|
-
log("Ignoring handshake frame",
|
|
75
|
+
log("Ignoring handshake frame", message.type, "in status", status.get());
|
|
160
76
|
});
|
|
161
77
|
};
|
|
162
|
-
const handleEncryptedFrame = async (
|
|
163
|
-
await match({ msg,
|
|
78
|
+
const handleEncryptedFrame = async (message) => {
|
|
79
|
+
await match({ msg: message, status: status.get(), isHost })
|
|
164
80
|
// Host: client responded with its public key.
|
|
165
|
-
.with({ msg: { type: "pubkey" }, isHost: true,
|
|
166
|
-
|
|
81
|
+
.with({ msg: { type: "pubkey" }, isHost: true, status: Status.HANDSHAKE }, async ({ msg: { payload: messagePayload } }) => {
|
|
82
|
+
const receivedKey = await parseEncryptionKey(messagePayload.publicKey);
|
|
83
|
+
if (!setPeerKey(receivedKey))
|
|
167
84
|
return;
|
|
168
|
-
|
|
169
|
-
return await
|
|
85
|
+
setStatus(Status.HANDSHAKE_PARTIAL);
|
|
86
|
+
return await send("encrypted", capabilitiesMessage());
|
|
170
87
|
})
|
|
171
|
-
.with({ msg: { type: "capabilities" },
|
|
172
|
-
await
|
|
173
|
-
|
|
88
|
+
.with({ msg: { type: "capabilities" }, status: Status.HANDSHAKE_PARTIAL }, async ({ msg: { payload: messagePayload } }) => {
|
|
89
|
+
await setPeerCapabilities(messagePayload);
|
|
90
|
+
setStatus(Status.ENCRYPTED);
|
|
174
91
|
if (isHost)
|
|
175
92
|
return;
|
|
176
|
-
return await send("encrypted",
|
|
93
|
+
return await send("encrypted", capabilitiesMessage());
|
|
177
94
|
})
|
|
178
95
|
// Client already encrypted, but the host is still re-sending its
|
|
179
96
|
// capabilities (our final packet was lost): answer again so the host
|
|
180
97
|
// can finish.
|
|
181
|
-
.with({ msg: { type: "capabilities" },
|
|
182
|
-
.with({ msg: { type: "data" },
|
|
183
|
-
emitter.emit("message",
|
|
98
|
+
.with({ msg: { type: "capabilities" }, status: Status.ENCRYPTED, isHost: false }, async () => await send("encrypted", capabilitiesMessage()))
|
|
99
|
+
.with({ msg: { type: "data" }, status: Status.ENCRYPTED }, async () => {
|
|
100
|
+
emitter.emit("message", message.payload);
|
|
184
101
|
})
|
|
185
102
|
.otherwise(() => {
|
|
186
|
-
log("Ignoring encrypted frame",
|
|
103
|
+
log("Ignoring encrypted frame", message.type, "in status", status.get());
|
|
187
104
|
});
|
|
188
105
|
};
|
|
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.
|
|
106
|
+
const onReceive = async (payload) => {
|
|
193
107
|
try {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const body = payload.slice(2);
|
|
197
|
-
const isRecipient = (isHost ? "h" : "c") === recipient;
|
|
198
|
-
if (!isRecipient)
|
|
108
|
+
const parsed = await parse(payload);
|
|
109
|
+
if (!parsed)
|
|
199
110
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
else if (prefix === XR_PREFIX) {
|
|
208
|
-
const msg = parseSignalMessage(await decrypt(body));
|
|
209
|
-
if (msg)
|
|
210
|
-
await handleEncryptedFrame(msg);
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
log("Dropping frame with unknown prefix");
|
|
214
|
-
}
|
|
111
|
+
const [stage, message] = parsed;
|
|
112
|
+
if (stage === XR_H_PREFIX && message)
|
|
113
|
+
return await handleHandshakeFrame(message);
|
|
114
|
+
if (stage === XR_PREFIX && message)
|
|
115
|
+
return await handleEncryptedFrame(message);
|
|
116
|
+
log("Dropping frame with unknown prefix");
|
|
215
117
|
}
|
|
216
118
|
catch (error) {
|
|
217
119
|
log("Dropping undecryptable or malformed frame", error);
|
|
218
120
|
}
|
|
219
121
|
};
|
|
122
|
+
const setup = async () => {
|
|
123
|
+
setStatus(Status.CONNECTING);
|
|
124
|
+
await channel.setup();
|
|
125
|
+
await channel.subscribe(onReceive);
|
|
126
|
+
if (canEncrypt()) {
|
|
127
|
+
setStatus(Status.ENCRYPTED);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
setStatus(Status.READY);
|
|
131
|
+
if (!isHost) {
|
|
132
|
+
// Enter HANDSHAKE before publishing: the host's pubkey reply can
|
|
133
|
+
// arrive while the publish is still in flight.
|
|
134
|
+
setStatus(Status.HANDSHAKE);
|
|
135
|
+
await send("handshake", {
|
|
136
|
+
type: "flash",
|
|
137
|
+
payload: {},
|
|
138
|
+
timestamp: Date.now(),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const teardown = async () => {
|
|
143
|
+
log("teardown");
|
|
144
|
+
await channel.teardown?.();
|
|
145
|
+
};
|
|
220
146
|
return make(emitter, {
|
|
221
|
-
type:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
await init.setup();
|
|
225
|
-
await init.subscribe(handleReceive);
|
|
226
|
-
if (canEncrypt()) {
|
|
227
|
-
setState(SIGNAL_STATE.ENCRYPTED);
|
|
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
|
-
},
|
|
147
|
+
type: channel.type,
|
|
148
|
+
setup,
|
|
149
|
+
teardown,
|
|
248
150
|
send(message) {
|
|
249
151
|
if (!canEncrypt()) {
|
|
250
152
|
return Promise.reject(new Error("Cannot encrypt message before keys are exchanged"));
|
|
251
153
|
}
|
|
252
|
-
|
|
253
|
-
return send("encrypted", them, {
|
|
154
|
+
return send("encrypted", {
|
|
254
155
|
type: "data",
|
|
255
156
|
payload: message,
|
|
256
157
|
timestamp: Date.now(),
|
|
257
158
|
});
|
|
258
159
|
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
160
|
+
status,
|
|
161
|
+
peerKey,
|
|
162
|
+
peerCapabilities,
|
|
262
163
|
});
|
|
263
164
|
};
|
|
264
165
|
//# 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 SignalingLayerFn = (\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): SignalingLayerFn => 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 peerKeyRecorded = false;\n let peerCapabilitiesRecorded = 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 (peerKeyRecorded) return false;\n\n peerKeyRecorded = 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 (peerCapabilitiesRecorded) return;\n\n peerCapabilitiesRecorded = 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 (msg: SignalMessage) => {\n await match({ msg, 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: msgPayload } }) => {\n try {\n const receivedKey = await parseEncryptionKey(msgPayload.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(msgPayload.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\", msg.type, \"in state\", state);\n });\n };\n\n const handleEncryptedFrame = async (msg: SignalMessage) => {\n await match({ msg, 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: msgPayload } }) => {\n if (!await recordPeerKey(msgPayload.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: msgPayload } }) => {\n await recordPeerCapabilities(msgPayload);\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\", msg.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", msg.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 msg = parseSignalMessage(await handshakeKey.decrypt(body));\n\n if (msg) await handleHandshakeFrame(msg);\n }\n else if (prefix === XR_PREFIX) {\n const msg = parseSignalMessage(await decrypt(body));\n\n if (msg) await handleEncryptedFrame(msg);\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 { make, type Observable, observable } from \"@openlv/core\";\nimport type { EncryptionKey, SymmetricKey } from \"@openlv/core/encryption\";\nimport { parseEncryptionKey, validatePublicKeyHash } from \"@openlv/core/encryption\";\nimport { EventEmitter } from \"eventemitter3\";\nimport { match } from \"ts-pattern\";\n\nimport { handshake, XR_H_PREFIX, XR_PREFIX } from \"./handshake.js\";\nimport {\n type PeerCapabilities,\n type SignalMessage,\n} from \"./messages.js\";\nimport type { SignalingChannel } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nexport * from \"./messages.js\";\nexport * from \"./protocol.js\";\n\nexport const Status = {\n STANDBY: \"standby\",\n CONNECTING: \"connecting\",\n READY: \"ready\",\n HANDSHAKE: \"handshake\",\n HANDSHAKE_PARTIAL: \"handshake-partial\",\n ENCRYPTED: \"encrypted\",\n ERROR: \"error\",\n} as const;\nexport type Status = (typeof Status)[keyof typeof Status];\n\nexport type SignalEventMap = {\n message: (message: object) => void;\n};\n\nexport type SignalingHooks = {\n isHost: boolean;\n h: string;\n k: SymmetricKey;\n // Our capabilities, advertised to the peer during the handshake\n capabilities: PeerCapabilities;\n // Decrypt using our private key\n decrypt: (message: string) => Promise<string>;\n // Encrypt to relying party\n encrypt: (message: string) => Promise<string>;\n // our public key\n publicKey: EncryptionKey;\n canEncrypt: () => boolean;\n};\n\nexport type SignalingContext = {\n type: string;\n\n // Sending only works once keys are exchanged\n send: (message: object) => Promise<void>;\n setup: () => Promise<void>;\n teardown: () => Promise<void>;\n\n status: Observable<Status>;\n peerKey: Observable<EncryptionKey | undefined>;\n peerCapabilities: Observable<PeerCapabilities | undefined>;\n};\nexport type SignalingLayer = EventEmitter<SignalEventMap> & SignalingContext;\nexport type SignalingLayerFunction = (\n hooks: SignalingHooks,\n) => Promise<SignalingLayer>;\n\nexport type CreateSignalingLayerFunction = (\n base: SignalingChannel,\n) => SignalingLayerFunction;\n\n/**\n * Base Signaling Layer implementation\n *\n * https://openlv.sh/api/signaling\n */\nexport const createSignalingLayer: CreateSignalingLayerFunction = channel => async (hooks: SignalingHooks) => {\n const {\n canEncrypt,\n encrypt,\n decrypt,\n capabilities,\n h,\n k: handshakeKey,\n publicKey,\n isHost,\n } = hooks;\n\n const [status, setStatus] = observable<Status>(Status.STANDBY);\n const [peerKey, setPeerKey] = observable<EncryptionKey | undefined>(undefined);\n const [peerCapabilities, setPeerCapabilities] = observable<PeerCapabilities | undefined>(undefined);\n\n const emitter = new EventEmitter<SignalEventMap>();\n\n const { frame, parse } = handshake({\n isHost,\n handshakeKey,\n encrypt,\n decrypt,\n });\n\n const send = async (method: \"handshake\" | \"encrypted\", payload: SignalMessage) => await channel.publish(await frame(method, payload));\n\n const capabilitiesMessage = (): SignalMessage => ({\n type: \"capabilities\",\n payload: capabilities,\n timestamp: Date.now(),\n });\n const pubkeyMessage = (): SignalMessage => ({\n type: \"pubkey\",\n payload: { publicKey: publicKey.toString() },\n timestamp: Date.now(),\n });\n\n const handleHandshakeFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n .with({ msg: { type: \"flash\" }, status: Status.READY, isHost: true }, async () => {\n setStatus(Status.HANDSHAKE);\n await send(\"handshake\", pubkeyMessage());\n })\n .with({ msg: { type: \"pubkey\" }, status: Status.HANDSHAKE, isHost: false }, async ({ msg: { payload: messagePayload } }) => {\n let receivedKey: EncryptionKey;\n\n try {\n receivedKey = await parseEncryptionKey(messagePayload.publicKey);\n\n if (!await validatePublicKeyHash(receivedKey, h)) {\n setStatus(Status.ERROR);\n log(\"Received host public key does not match expected hash -- possible tampering\");\n\n return;\n }\n }\n catch {\n setStatus(Status.ERROR);\n log(\"Failed to parse received host public key\");\n\n return;\n }\n\n const changed = setPeerKey(receivedKey)\n && setStatus(Status.HANDSHAKE_PARTIAL);\n\n if (!changed) return;\n\n return await send(\"encrypted\", pubkeyMessage());\n })\n .otherwise(() => {\n log(\"Ignoring handshake frame\", message.type, \"in status\", status.get());\n });\n };\n\n const handleEncryptedFrame = async (message: SignalMessage) => {\n await match({ msg: message, status: status.get(), isHost })\n // Host: client responded with its public key.\n .with(\n { msg: { type: \"pubkey\" }, isHost: true, status: Status.HANDSHAKE },\n async ({ msg: { payload: messagePayload } }) => {\n const receivedKey = await parseEncryptionKey(messagePayload.publicKey);\n\n if (!setPeerKey(receivedKey)) return;\n\n setStatus(Status.HANDSHAKE_PARTIAL);\n\n return await send(\"encrypted\", capabilitiesMessage());\n },\n )\n .with(\n { msg: { type: \"capabilities\" }, status: Status.HANDSHAKE_PARTIAL },\n async ({ msg: { payload: messagePayload } }) => {\n await setPeerCapabilities(messagePayload);\n setStatus(Status.ENCRYPTED);\n\n if (isHost) return;\n\n return await send(\"encrypted\", capabilitiesMessage());\n },\n )\n // Client already encrypted, but the host is still re-sending its\n // capabilities (our final packet was lost): answer again so the host\n // can finish.\n .with(\n { msg: { type: \"capabilities\" }, status: Status.ENCRYPTED, isHost: false },\n async () => await send(\"encrypted\", capabilitiesMessage()),\n )\n .with({ msg: { type: \"data\" }, status: Status.ENCRYPTED }, async () => {\n emitter.emit(\"message\", message.payload as object);\n })\n .otherwise(() => {\n log(\"Ignoring encrypted frame\", message.type, \"in status\", status.get());\n });\n };\n\n const onReceive = async (payload: string) => {\n try {\n const parsed = await parse(payload);\n\n if (!parsed) return;\n\n const [stage, message] = parsed;\n\n if (stage === XR_H_PREFIX && message) return await handleHandshakeFrame(message);\n\n if (stage === XR_PREFIX && message) return await handleEncryptedFrame(message);\n\n log(\"Dropping frame with unknown prefix\");\n }\n catch (error) {\n log(\"Dropping undecryptable or malformed frame\", error);\n }\n };\n\n const setup = async () => {\n setStatus(Status.CONNECTING);\n await channel.setup();\n await channel.subscribe(onReceive);\n\n if (canEncrypt()) {\n setStatus(Status.ENCRYPTED);\n\n return;\n }\n\n setStatus(Status.READY);\n\n if (!isHost) {\n // Enter HANDSHAKE before publishing: the host's pubkey reply can\n // arrive while the publish is still in flight.\n setStatus(Status.HANDSHAKE);\n await send(\"handshake\", {\n type: \"flash\",\n payload: {},\n timestamp: Date.now(),\n });\n }\n };\n\n const teardown = async () => {\n log(\"teardown\");\n await channel.teardown?.();\n };\n\n return make(emitter, {\n type: channel.type,\n setup,\n teardown,\n send(message: object) {\n if (!canEncrypt()) {\n return Promise.reject(\n new Error(\"Cannot encrypt message before keys are exchanged\"),\n );\n }\n\n return send(\"encrypted\", {\n type: \"data\",\n payload: message,\n timestamp: Date.now(),\n });\n },\n status,\n peerKey,\n peerCapabilities,\n });\n};\n"]}
|
package/dist/index.test.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { generateKeyPair } from "@openlv/core/encryption";
|
|
1
|
+
import { deriveSymmetricKey, generateKeyPair } from "@openlv/core/encryption";
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import { mqtt } from "./mqtt/index.js";
|
|
4
4
|
import { ntfy } from "./ntfy/index.js";
|
|
5
5
|
import { log } from "./utils/log.js";
|
|
6
6
|
const hKey = "test";
|
|
7
|
+
const K = "00112233445566778899aabbccddeeff";
|
|
7
8
|
const providersByType = [
|
|
8
9
|
[
|
|
9
10
|
"mqtt",
|
|
@@ -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
|
// [
|
|
@@ -69,32 +70,35 @@ const providersByType = [
|
|
|
69
70
|
// ],
|
|
70
71
|
// ],
|
|
71
72
|
];
|
|
72
|
-
const testSignalingLayer = async (layer,
|
|
73
|
+
const testSignalingLayer = async (layer, properties) => {
|
|
73
74
|
const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();
|
|
74
75
|
const h = hKey;
|
|
75
|
-
const
|
|
76
|
+
const k = await deriveSymmetricKey(K);
|
|
77
|
+
const signalingLayer = await layer(properties);
|
|
76
78
|
const signalA = await signalingLayer({
|
|
77
79
|
h,
|
|
78
|
-
|
|
80
|
+
k,
|
|
79
81
|
capabilities: { transports: ["wrtc"] },
|
|
80
|
-
peerCapabilities: v => log("peerCapabilities", v),
|
|
81
82
|
canEncrypt: () => true,
|
|
82
83
|
encrypt: publicKey.encrypt,
|
|
83
84
|
decrypt: decryptionKey.decrypt,
|
|
84
85
|
publicKey,
|
|
85
86
|
isHost: true,
|
|
86
87
|
});
|
|
88
|
+
signalA.peerKey.subscribe(v => log("rpKey", v));
|
|
89
|
+
signalA.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
87
90
|
const signalB = await signalingLayer({
|
|
88
91
|
h,
|
|
89
|
-
|
|
92
|
+
k,
|
|
90
93
|
capabilities: { transports: ["wrtc"] },
|
|
91
|
-
peerCapabilities: v => log("peerCapabilities", v),
|
|
92
94
|
canEncrypt: () => true,
|
|
93
95
|
encrypt: publicKey.encrypt,
|
|
94
96
|
decrypt: decryptionKey.decrypt,
|
|
95
97
|
publicKey,
|
|
96
98
|
isHost: false,
|
|
97
99
|
});
|
|
100
|
+
signalB.peerKey.subscribe(v => log("rpKey", v));
|
|
101
|
+
signalB.peerCapabilities.subscribe(v => log("peerCapabilities", v));
|
|
98
102
|
expect(signalA).toBeDefined();
|
|
99
103
|
expect(signalB).toBeDefined();
|
|
100
104
|
await Promise.all([signalA.setup(), signalB.setup()]);
|
|
@@ -120,17 +124,17 @@ const testSignalingLayer = async (layer, props) => {
|
|
|
120
124
|
await signalB.teardown();
|
|
121
125
|
};
|
|
122
126
|
const PROVIDER_TIMEOUT_MS = 5000;
|
|
123
|
-
const testProvider = async (layer,
|
|
127
|
+
const testProvider = async (layer, properties) => {
|
|
124
128
|
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)), PROVIDER_TIMEOUT_MS));
|
|
125
129
|
const startTime = performance.now();
|
|
126
130
|
try {
|
|
127
|
-
await Promise.race([testSignalingLayer(layer,
|
|
131
|
+
await Promise.race([testSignalingLayer(layer, properties), timeout]);
|
|
128
132
|
const duration = Math.round(performance.now() - startTime);
|
|
129
|
-
return { url:
|
|
133
|
+
return { url: properties.url, ok: true, duration };
|
|
130
134
|
}
|
|
131
135
|
catch (error) {
|
|
132
136
|
return {
|
|
133
|
-
url:
|
|
137
|
+
url: properties.url,
|
|
134
138
|
ok: false,
|
|
135
139
|
error: error.message,
|
|
136
140
|
};
|
|
@@ -146,7 +150,7 @@ const formatResults = (typeName, results) => {
|
|
|
146
150
|
};
|
|
147
151
|
describe.for(providersByType)("signaling: %s", ([typeName, providers]) => {
|
|
148
152
|
it("should pass signaling messages between peers", { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 }, async () => {
|
|
149
|
-
const results = await Promise.all(providers.map(([, layer,
|
|
153
|
+
const results = await Promise.all(providers.map(([, layer, properties]) => testProvider(layer, properties)));
|
|
150
154
|
const summary = formatResults(typeName, results);
|
|
151
155
|
console.log(`\n${summary}`);
|
|
152
156
|
const passed = results.filter(r => r.ok).length;
|
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 props: SignalingProtocolOptions,\n): Promise<void> => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const h = hKey;\n\n const signalingLayer = await layer(props);\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 props: 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, props), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: props.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: props.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, props]) => testProvider(layer, props)),\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 { deriveSymmetricKey, generateKeyPair } from \"@openlv/core/encryption\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { mqtt } from \"./mqtt/index.js\";\nimport { ntfy } from \"./ntfy/index.js\";\nimport type { SignalingProtocol, SignalingProtocolOptions } from \"./protocol.js\";\nimport { log } from \"./utils/log.js\";\n\nconst hKey = \"test\";\nconst K = \"00112233445566778899aabbccddeeff\";\n\nconst providersByType: readonly [\n string,\n readonly [string, SignalingProtocol, SignalingProtocolOptions][],\n][] = [\n [\n \"mqtt\",\n [\n [\n \"mqtt-dashboard\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://mqtt-dashboard.com:8884/mqtt\",\n },\n ],\n [\n \"broker.emqx.io\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://broker.emqx.io:8083/mqtt\",\n },\n ],\n [\n \"test.mosquitto.org\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"ws://test.mosquitto.org:8080/mqtt\",\n },\n ],\n [\n \"broker.itdata.nu\",\n mqtt,\n {\n topic: \"mytesttopic1111\",\n url: \"wss://broker.itdata.nu/mqtt\",\n },\n ],\n ],\n ],\n [\n \"ntfy\",\n [\n [\n \"ntfy.sh\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.sh/\" },\n ],\n [\n \"ntfy.envs.net\",\n ntfy,\n { topic: \"mytesttopic1111\", url: \"https://ntfy.envs.net/\" },\n ],\n ],\n ],\n // gundb is deprecated and its public relays are too lossy to assert on.\n // [\n // \"gundb\",\n // [\n // [\n // \"try.axe.eco\",\n // gundb,\n // { topic: \"mytesttopic1111\", url: \"wss://try.axe.eco/gun\" },\n // ],\n // ],\n // ],\n] as const;\n\nconst testSignalingLayer = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<void> => {\n const { encryptionKey: publicKey, decryptionKey } = await generateKeyPair();\n const h = hKey;\n const k = await deriveSymmetricKey(K);\n\n const signalingLayer = await layer(properties);\n const signalA = await signalingLayer({\n h,\n k,\n capabilities: { transports: [\"wrtc\"] },\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: true,\n });\n\n signalA.peerKey.subscribe(v => log(\"rpKey\", v));\n signalA.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n const signalB = await signalingLayer({\n h,\n k,\n capabilities: { transports: [\"wrtc\"] },\n canEncrypt: () => true,\n encrypt: publicKey.encrypt,\n decrypt: decryptionKey.decrypt,\n publicKey,\n isHost: false,\n });\n\n signalB.peerKey.subscribe(v => log(\"rpKey\", v));\n signalB.peerCapabilities.subscribe(v => log(\"peerCapabilities\", v));\n\n expect(signalA).toBeDefined();\n expect(signalB).toBeDefined();\n\n await Promise.all([signalA.setup(), signalB.setup()]);\n\n const messageReceivedA = new Promise<void>((resolve) => {\n signalA.on(\"message\", (message) => {\n log(\"messageReceivedA\", message);\n expect(message).toEqual({ data: \"test2\" });\n resolve();\n });\n });\n const messageReceivedB = new Promise<void>((resolve) => {\n signalB.on(\"message\", (message) => {\n log(\"messageReceivedB\", message);\n expect(message).toEqual({ data: \"test1\" });\n resolve();\n });\n });\n\n await signalA.send({ data: \"test1\" });\n\n await messageReceivedB;\n\n await signalB.send({ data: \"test2\" });\n\n await messageReceivedA;\n\n await signalA.teardown();\n await signalB.teardown();\n};\n\nconst PROVIDER_TIMEOUT_MS = 5000;\n\ntype ProviderResult\n = | { url: string; ok: true; duration: number; }\n | { url: string; ok: false; error: string; };\n\nconst testProvider = async (\n layer: SignalingProtocol,\n properties: SignalingProtocolOptions,\n): Promise<ProviderResult> => {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(\n () => reject(new Error(`Timeout after ${PROVIDER_TIMEOUT_MS}ms`)),\n PROVIDER_TIMEOUT_MS,\n ),\n );\n\n const startTime = performance.now();\n\n try {\n await Promise.race([testSignalingLayer(layer, properties), timeout]);\n const duration = Math.round(performance.now() - startTime);\n\n return { url: properties.url, ok: true, duration };\n }\n catch (error) {\n return {\n url: properties.url,\n ok: false,\n error: (error as Error).message,\n };\n }\n};\n\nconst formatResults = (typeName: string, results: ProviderResult[]): string => {\n const passed = results.filter(r => r.ok).length;\n const total = results.length;\n\n return [\n `${typeName.toUpperCase()}: ${passed}/${total}`,\n ...results.map(r =>\n (r.ok ? `✓ ${r.url} ${r.duration}ms` : `✗ ${r.url} - ${r.error}`),\n ),\n ].join(\"\\n\");\n};\n\ndescribe.for(providersByType)(\"signaling: %s\", ([typeName, providers]) => {\n it(\n \"should pass signaling messages between peers\",\n { timeout: PROVIDER_TIMEOUT_MS * providers.length + 5000 },\n async () => {\n const results = await Promise.all(\n providers.map(([, layer, properties]) => testProvider(layer, properties)),\n );\n\n const summary = formatResults(typeName, results);\n\n console.log(`\\n${summary}`);\n\n const passed = results.filter(r => r.ok).length;\n\n expect(passed).toBeGreaterThan(0);\n },\n );\n});\n"]}
|
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;
|