@openlv/session 0.0.1-beta.2 → 0.0.1-beta.3
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/package.json +3 -3
- package/dist/connection.d.ts +0 -57
- package/dist/connection.js +0 -757
- package/dist/connection.js.map +0 -1
- package/dist/encryption/asymmetric.d.ts +0 -22
- package/dist/encryption/asymmetric.js +0 -90
- package/dist/encryption/asymmetric.js.map +0 -1
- package/dist/encryption/asymmetric.test.d.ts +0 -1
- package/dist/encryption/asymmetric.test.js +0 -38
- package/dist/encryption/asymmetric.test.js.map +0 -1
- package/dist/encryption/base64.d.ts +0 -2
- package/dist/encryption/base64.js +0 -16
- package/dist/encryption/base64.js.map +0 -1
- package/dist/encryption/base64.test.d.ts +0 -1
- package/dist/encryption/base64.test.js +0 -21
- package/dist/encryption/base64.test.js.map +0 -1
- package/dist/encryption/handshake.d.ts +0 -10
- package/dist/encryption/handshake.js +0 -37
- package/dist/encryption/handshake.js.map +0 -1
- package/dist/encryption/handshake.test.d.ts +0 -1
- package/dist/encryption/handshake.test.js +0 -16
- package/dist/encryption/handshake.test.js.map +0 -1
- package/dist/encryption/hash.d.ts +0 -13
- package/dist/encryption/hash.js +0 -32
- package/dist/encryption/hash.js.map +0 -1
- package/dist/encryption/index.d.ts +0 -4
- package/dist/encryption/index.js +0 -5
- package/dist/encryption/index.js.map +0 -1
- package/dist/encryption/random.d.ts +0 -8
- package/dist/encryption/random.js +0 -21
- package/dist/encryption/random.js.map +0 -1
- package/dist/provider.d.ts +0 -11
- package/dist/provider.js +0 -285
- package/dist/provider.js.map +0 -1
- package/dist/session/index.d.ts +0 -41
- package/dist/session/index.js +0 -99
- package/dist/session/index.js.map +0 -1
- package/dist/session/index.test.d.ts +0 -1
- package/dist/session/index.test.js +0 -32
- package/dist/session/index.test.js.map +0 -1
- package/dist/signaling/base.d.ts +0 -14
- package/dist/signaling/base.js +0 -2
- package/dist/signaling/base.js.map +0 -1
- package/dist/signaling/index.d.ts +0 -28
- package/dist/signaling/index.js +0 -152
- package/dist/signaling/index.js.map +0 -1
- package/dist/signaling/index.test.d.ts +0 -1
- package/dist/signaling/index.test.js +0 -62
- package/dist/signaling/index.test.js.map +0 -1
- package/dist/signaling/messages/index.d.ts +0 -48
- package/dist/signaling/messages/index.js +0 -2
- package/dist/signaling/messages/index.js.map +0 -1
- package/dist/signaling/mqtt/index.d.ts +0 -5
- package/dist/signaling/mqtt/index.js +0 -49
- package/dist/signaling/mqtt/index.js.map +0 -1
- package/dist/signaling/mqtt.d.ts +0 -31
- package/dist/signaling/mqtt.js +0 -152
- package/dist/signaling/mqtt.js.map +0 -1
- package/dist/signaling/ntfy/index.d.ts +0 -25
- package/dist/signaling/ntfy/index.js +0 -92
- package/dist/signaling/ntfy/index.js.map +0 -1
- package/dist/signaling/ntfy/url.d.ts +0 -12
- package/dist/signaling/ntfy/url.js +0 -37
- package/dist/signaling/ntfy/url.js.map +0 -1
- package/dist/signaling/ntfy/url.test.d.ts +0 -1
- package/dist/signaling/ntfy/url.test.js +0 -54
- package/dist/signaling/ntfy/url.test.js.map +0 -1
- package/dist/transport/index.d.ts +0 -11
- package/dist/transport/index.js +0 -8
- package/dist/transport/index.js.map +0 -1
- package/dist/transport/webrtc/index.d.ts +0 -1
- package/dist/transport/webrtc/index.js +0 -70
- package/dist/transport/webrtc/index.js.map +0 -1
- package/dist/utils/encryption.d.ts +0 -86
- package/dist/utils/encryption.js +0 -243
- package/dist/utils/encryption.js.map +0 -1
- package/dist/utils/url.d.ts +0 -3
- package/dist/utils/url.js +0 -66
- package/dist/utils/url.js.map +0 -1
- package/dist/utils/url.test.d.ts +0 -1
- package/dist/utils/url.test.js +0 -29
- package/dist/utils/url.test.js.map +0 -1
package/dist/connection.js
DELETED
|
@@ -1,757 +0,0 @@
|
|
|
1
|
-
/* eslint-disable sonarjs/cognitive-complexity */
|
|
2
|
-
/* eslint-disable sonarjs/no-duplicate-string */
|
|
3
|
-
import { MQTTSignaling } from './signaling/mqtt.js';
|
|
4
|
-
import { EncryptionUtils } from './utils/encryption.js';
|
|
5
|
-
export const encodeConnectionURL = (payload) => {
|
|
6
|
-
const params = new URLSearchParams();
|
|
7
|
-
params.set('h', payload.pubkeyHash);
|
|
8
|
-
params.set('k', payload.sharedKey);
|
|
9
|
-
if (payload.server) {
|
|
10
|
-
params.set('s', payload.server);
|
|
11
|
-
}
|
|
12
|
-
if (payload.protocol && payload.protocol !== 'mqtt') {
|
|
13
|
-
params.set('p', payload.protocol);
|
|
14
|
-
}
|
|
15
|
-
return `openlv://${payload.sessionId}?${params.toString()}`;
|
|
16
|
-
};
|
|
17
|
-
export const decodeConnectionURL = (url) => {
|
|
18
|
-
// Type check and validation
|
|
19
|
-
if (typeof url !== 'string') {
|
|
20
|
-
throw new Error(`Invalid URL type: expected string, got ${typeof url}`);
|
|
21
|
-
}
|
|
22
|
-
if (!url || url.trim() === '') {
|
|
23
|
-
throw new Error('URL cannot be empty');
|
|
24
|
-
}
|
|
25
|
-
if (!url.startsWith('openlv://')) {
|
|
26
|
-
throw new Error(`Invalid URL format: must start with 'openlv://', got: ${url}`);
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const urlObj = new URL(url);
|
|
30
|
-
const sessionId = urlObj.hostname || urlObj.pathname.replace('/', '');
|
|
31
|
-
const pubkeyHash = urlObj.searchParams.get('h') || '';
|
|
32
|
-
const sharedKey = urlObj.searchParams.get('k') || '';
|
|
33
|
-
const server = urlObj.searchParams.get('s') || undefined;
|
|
34
|
-
const protocol = urlObj.searchParams.get('p') || 'mqtt';
|
|
35
|
-
if (!sessionId) {
|
|
36
|
-
throw new Error('Session ID is required in URL');
|
|
37
|
-
}
|
|
38
|
-
// Validate session ID format (16 characters, URL-safe alphabet)
|
|
39
|
-
if (!/^[A-Za-z0-9_-]{16}$/.test(sessionId)) {
|
|
40
|
-
throw new Error('Invalid session ID format: must be 16 URL-safe characters');
|
|
41
|
-
}
|
|
42
|
-
if (!pubkeyHash) {
|
|
43
|
-
throw new Error('Public key hash (h parameter) is required in URL');
|
|
44
|
-
}
|
|
45
|
-
// Validate public key hash format (16 hex characters)
|
|
46
|
-
if (!/^[0-9a-f]{16}$/.test(pubkeyHash)) {
|
|
47
|
-
throw new Error('Invalid public key hash format: must be 16 hex characters');
|
|
48
|
-
}
|
|
49
|
-
if (!sharedKey) {
|
|
50
|
-
throw new Error('Shared key (k parameter) is required in URL');
|
|
51
|
-
}
|
|
52
|
-
// Validate shared key format (64 hex characters)
|
|
53
|
-
if (!/^[0-9a-f]{64}$/.test(sharedKey)) {
|
|
54
|
-
throw new Error('Invalid shared key format: must be 64 hex characters');
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
sessionId,
|
|
58
|
-
pubkeyHash,
|
|
59
|
-
sharedKey,
|
|
60
|
-
server: server ? decodeURIComponent(server) : undefined,
|
|
61
|
-
protocol,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
// If it's already our custom error, re-throw it
|
|
66
|
-
if (error instanceof Error && error.message.includes('required')) {
|
|
67
|
-
throw error;
|
|
68
|
-
}
|
|
69
|
-
// Fallback to legacy regex parsing
|
|
70
|
-
try {
|
|
71
|
-
const legacyMatch = url.match(/openlv:\/\/([^?]+)\?(?:sharedKey|k|h)=([^&]+)/) ?? [];
|
|
72
|
-
const [, sessionId, keyOrHash] = legacyMatch;
|
|
73
|
-
if (!sessionId || !keyOrHash) {
|
|
74
|
-
throw new Error(`Invalid URL format: could not parse session ID or key from ${url}`);
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
sessionId,
|
|
78
|
-
pubkeyHash: keyOrHash, // Assume it's pubkeyHash for legacy
|
|
79
|
-
sharedKey: keyOrHash, // Use same value for both in legacy mode
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
84
|
-
throw new Error(`Failed to parse URL: ${url}. Original error: ${errorMessage}`);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
export class OpenLVConnection {
|
|
89
|
-
constructor(config) {
|
|
90
|
-
Object.defineProperty(this, "sessionId", {
|
|
91
|
-
enumerable: true,
|
|
92
|
-
configurable: true,
|
|
93
|
-
writable: true,
|
|
94
|
-
value: void 0
|
|
95
|
-
});
|
|
96
|
-
Object.defineProperty(this, "pubkeyHash", {
|
|
97
|
-
enumerable: true,
|
|
98
|
-
configurable: true,
|
|
99
|
-
writable: true,
|
|
100
|
-
value: void 0
|
|
101
|
-
});
|
|
102
|
-
Object.defineProperty(this, "sharedKey", {
|
|
103
|
-
enumerable: true,
|
|
104
|
-
configurable: true,
|
|
105
|
-
writable: true,
|
|
106
|
-
value: void 0
|
|
107
|
-
});
|
|
108
|
-
Object.defineProperty(this, "privateKey", {
|
|
109
|
-
enumerable: true,
|
|
110
|
-
configurable: true,
|
|
111
|
-
writable: true,
|
|
112
|
-
value: void 0
|
|
113
|
-
});
|
|
114
|
-
Object.defineProperty(this, "publicKey", {
|
|
115
|
-
enumerable: true,
|
|
116
|
-
configurable: true,
|
|
117
|
-
writable: true,
|
|
118
|
-
value: void 0
|
|
119
|
-
});
|
|
120
|
-
Object.defineProperty(this, "peerPublicKey", {
|
|
121
|
-
enumerable: true,
|
|
122
|
-
configurable: true,
|
|
123
|
-
writable: true,
|
|
124
|
-
value: void 0
|
|
125
|
-
});
|
|
126
|
-
Object.defineProperty(this, "peerConnection", {
|
|
127
|
-
enumerable: true,
|
|
128
|
-
configurable: true,
|
|
129
|
-
writable: true,
|
|
130
|
-
value: void 0
|
|
131
|
-
});
|
|
132
|
-
Object.defineProperty(this, "dataChannel", {
|
|
133
|
-
enumerable: true,
|
|
134
|
-
configurable: true,
|
|
135
|
-
writable: true,
|
|
136
|
-
value: void 0
|
|
137
|
-
});
|
|
138
|
-
Object.defineProperty(this, "signaling", {
|
|
139
|
-
enumerable: true,
|
|
140
|
-
configurable: true,
|
|
141
|
-
writable: true,
|
|
142
|
-
value: void 0
|
|
143
|
-
});
|
|
144
|
-
Object.defineProperty(this, "isInitiator", {
|
|
145
|
-
enumerable: true,
|
|
146
|
-
configurable: true,
|
|
147
|
-
writable: true,
|
|
148
|
-
value: false
|
|
149
|
-
});
|
|
150
|
-
Object.defineProperty(this, "connectionState", {
|
|
151
|
-
enumerable: true,
|
|
152
|
-
configurable: true,
|
|
153
|
-
writable: true,
|
|
154
|
-
value: 'disconnected'
|
|
155
|
-
});
|
|
156
|
-
Object.defineProperty(this, "pendingICECandidates", {
|
|
157
|
-
enumerable: true,
|
|
158
|
-
configurable: true,
|
|
159
|
-
writable: true,
|
|
160
|
-
value: []
|
|
161
|
-
});
|
|
162
|
-
Object.defineProperty(this, "webrtcRetryCount", {
|
|
163
|
-
enumerable: true,
|
|
164
|
-
configurable: true,
|
|
165
|
-
writable: true,
|
|
166
|
-
value: 0
|
|
167
|
-
});
|
|
168
|
-
Object.defineProperty(this, "maxWebrtcRetries", {
|
|
169
|
-
enumerable: true,
|
|
170
|
-
configurable: true,
|
|
171
|
-
writable: true,
|
|
172
|
-
value: 3
|
|
173
|
-
});
|
|
174
|
-
Object.defineProperty(this, "webrtcRetryInterval", {
|
|
175
|
-
enumerable: true,
|
|
176
|
-
configurable: true,
|
|
177
|
-
writable: true,
|
|
178
|
-
value: void 0
|
|
179
|
-
});
|
|
180
|
-
Object.defineProperty(this, "connectionTimeout", {
|
|
181
|
-
enumerable: true,
|
|
182
|
-
configurable: true,
|
|
183
|
-
writable: true,
|
|
184
|
-
value: void 0
|
|
185
|
-
});
|
|
186
|
-
// Event handlers
|
|
187
|
-
Object.defineProperty(this, "phaseHandlers", {
|
|
188
|
-
enumerable: true,
|
|
189
|
-
configurable: true,
|
|
190
|
-
writable: true,
|
|
191
|
-
value: []
|
|
192
|
-
});
|
|
193
|
-
Object.defineProperty(this, "messageHandlers", {
|
|
194
|
-
enumerable: true,
|
|
195
|
-
configurable: true,
|
|
196
|
-
writable: true,
|
|
197
|
-
value: []
|
|
198
|
-
});
|
|
199
|
-
Object.defineProperty(this, "errorHandlers", {
|
|
200
|
-
enumerable: true,
|
|
201
|
-
configurable: true,
|
|
202
|
-
writable: true,
|
|
203
|
-
value: []
|
|
204
|
-
});
|
|
205
|
-
// Connection state flags
|
|
206
|
-
Object.defineProperty(this, "hasSharedPublicKey", {
|
|
207
|
-
enumerable: true,
|
|
208
|
-
configurable: true,
|
|
209
|
-
writable: true,
|
|
210
|
-
value: false
|
|
211
|
-
});
|
|
212
|
-
// Peer identification
|
|
213
|
-
Object.defineProperty(this, "peerId", {
|
|
214
|
-
enumerable: true,
|
|
215
|
-
configurable: true,
|
|
216
|
-
writable: true,
|
|
217
|
-
value: void 0
|
|
218
|
-
});
|
|
219
|
-
this.signaling = new MQTTSignaling(config);
|
|
220
|
-
this.peerId = EncryptionUtils.generateSessionId(); // Unique identifier for this peer
|
|
221
|
-
}
|
|
222
|
-
updateConnectionState(newState, description) {
|
|
223
|
-
const oldState = this.connectionState;
|
|
224
|
-
this.connectionState = newState;
|
|
225
|
-
console.log(`Connection: ${oldState} → ${newState} (${description})`);
|
|
226
|
-
const phase = {
|
|
227
|
-
state: newState,
|
|
228
|
-
description,
|
|
229
|
-
timestamp: Date.now(),
|
|
230
|
-
};
|
|
231
|
-
// Notify phase handlers
|
|
232
|
-
this.phaseHandlers.forEach((handler) => handler(phase));
|
|
233
|
-
// Handle state transitions
|
|
234
|
-
if (newState === 'webrtc-connected' && oldState !== 'webrtc-connected') {
|
|
235
|
-
this.onWebRTCConnected();
|
|
236
|
-
}
|
|
237
|
-
else if (oldState === 'webrtc-connected' && newState !== 'webrtc-connected') {
|
|
238
|
-
this.onWebRTCDisconnected();
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
onWebRTCConnected() {
|
|
242
|
-
console.log('WebRTC connected - closing MQTT to save resources');
|
|
243
|
-
this.clearTimeouts();
|
|
244
|
-
this.signaling.disconnect();
|
|
245
|
-
}
|
|
246
|
-
onWebRTCDisconnected() {
|
|
247
|
-
console.log('WebRTC disconnected - reopening MQTT for fallback');
|
|
248
|
-
this.signaling.reconnect();
|
|
249
|
-
}
|
|
250
|
-
async handleSignalingMessage(message) {
|
|
251
|
-
try {
|
|
252
|
-
console.log(`Processing: ${message.type} (initiator: ${this.isInitiator})`);
|
|
253
|
-
// Ignore our own messages
|
|
254
|
-
if (message.senderId === this.peerId) {
|
|
255
|
-
console.log(`Ignoring own message: ${message.type}`);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
switch (message.type) {
|
|
259
|
-
case 'ping':
|
|
260
|
-
await this.handlePing();
|
|
261
|
-
break;
|
|
262
|
-
case 'pubkey':
|
|
263
|
-
await this.handlePublicKey(message);
|
|
264
|
-
break;
|
|
265
|
-
case 'hello':
|
|
266
|
-
await this.handleHello(message);
|
|
267
|
-
break;
|
|
268
|
-
case 'webrtc-offer':
|
|
269
|
-
await this.handleWebRTCOffer(message);
|
|
270
|
-
break;
|
|
271
|
-
case 'webrtc-answer':
|
|
272
|
-
await this.handleWebRTCAnswer(message);
|
|
273
|
-
break;
|
|
274
|
-
case 'ice-candidate':
|
|
275
|
-
await this.handleICECandidate(message);
|
|
276
|
-
break;
|
|
277
|
-
case 'data':
|
|
278
|
-
await this.handleDataMessage(message);
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
console.error('Error handling signaling message:', error);
|
|
284
|
-
this.errorHandlers.forEach((handler) => handler(error));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
async handlePing() {
|
|
288
|
-
if (this.isInitiator && !this.hasSharedPublicKey) {
|
|
289
|
-
console.log('Received ping from wallet - peer is requesting public key');
|
|
290
|
-
this.updateConnectionState('pairing', 'Wallet detected, sharing public key');
|
|
291
|
-
await this.sharePublicKey();
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
async handlePublicKey(message) {
|
|
295
|
-
if (this.isInitiator)
|
|
296
|
-
return; // Only wallet processes public keys
|
|
297
|
-
console.log('Received public key from dApp, verifying...');
|
|
298
|
-
this.updateConnectionState('key-exchange', 'Verifying dApp public key');
|
|
299
|
-
// First, compute hash from the raw base64 data to verify it matches
|
|
300
|
-
const publicKeyRaw = new Uint8Array(atob(message.payload.publicKey)
|
|
301
|
-
.split('')
|
|
302
|
-
.map((char) => char.charCodeAt(0)));
|
|
303
|
-
const computedHash = await EncryptionUtils.computePublicKeyHashFromRaw(publicKeyRaw);
|
|
304
|
-
if (computedHash !== this.pubkeyHash) {
|
|
305
|
-
throw new Error(`Public key hash mismatch! Expected: ${this.pubkeyHash}, Got: ${computedHash}`);
|
|
306
|
-
}
|
|
307
|
-
// Now import the key for use
|
|
308
|
-
const publicKeyData = await EncryptionUtils.publicKeyFromBase64(message.payload.publicKey);
|
|
309
|
-
this.peerPublicKey = publicKeyData;
|
|
310
|
-
this.updateConnectionState('key-exchange', 'Sending encrypted hello message');
|
|
311
|
-
await this.sendHello();
|
|
312
|
-
}
|
|
313
|
-
async handleHello(message) {
|
|
314
|
-
if (!this.isInitiator)
|
|
315
|
-
return; // Only dApp processes hello messages
|
|
316
|
-
console.log('Received hello from wallet');
|
|
317
|
-
this.updateConnectionState('key-exchange', 'Received wallet hello, starting WebRTC');
|
|
318
|
-
// Decrypt the hello message
|
|
319
|
-
const decryptedMessage = await EncryptionUtils.decryptMessage(message.payload, this.privateKey);
|
|
320
|
-
this.peerPublicKey = await EncryptionUtils.publicKeyFromBase64(decryptedMessage.publicKey);
|
|
321
|
-
this.updateConnectionState('webrtc-negotiating', 'Creating WebRTC offer');
|
|
322
|
-
await this.createWebRTCOffer();
|
|
323
|
-
}
|
|
324
|
-
async handleWebRTCOffer(message) {
|
|
325
|
-
if (this.isInitiator)
|
|
326
|
-
return;
|
|
327
|
-
console.log('Received WebRTC offer');
|
|
328
|
-
this.updateConnectionState('webrtc-negotiating', 'Processing WebRTC offer');
|
|
329
|
-
const decryptedOffer = await EncryptionUtils.decryptMessage(message.payload, this.privateKey);
|
|
330
|
-
await this.peerConnection.setRemoteDescription(decryptedOffer);
|
|
331
|
-
// Add any pending ICE candidates
|
|
332
|
-
for (const candidate of this.pendingICECandidates) {
|
|
333
|
-
await this.peerConnection.addIceCandidate(candidate);
|
|
334
|
-
}
|
|
335
|
-
this.pendingICECandidates = [];
|
|
336
|
-
const answer = await this.peerConnection.createAnswer();
|
|
337
|
-
await this.peerConnection.setLocalDescription(answer);
|
|
338
|
-
// Send encrypted answer
|
|
339
|
-
const encryptedAnswer = await EncryptionUtils.encryptMessage(answer, this.peerPublicKey, this.privateKey, this.publicKey);
|
|
340
|
-
await this.signaling.publish({
|
|
341
|
-
type: 'webrtc-answer',
|
|
342
|
-
payload: encryptedAnswer,
|
|
343
|
-
sessionId: this.sessionId,
|
|
344
|
-
timestamp: Date.now(),
|
|
345
|
-
senderId: this.peerId,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
async handleWebRTCAnswer(message) {
|
|
349
|
-
if (!this.isInitiator)
|
|
350
|
-
return;
|
|
351
|
-
console.log('Received WebRTC answer');
|
|
352
|
-
this.updateConnectionState('webrtc-negotiating', 'Processing WebRTC answer');
|
|
353
|
-
const decryptedAnswer = await EncryptionUtils.decryptMessage(message.payload, this.privateKey);
|
|
354
|
-
await this.peerConnection.setRemoteDescription(decryptedAnswer);
|
|
355
|
-
// Add any pending ICE candidates
|
|
356
|
-
for (const candidate of this.pendingICECandidates) {
|
|
357
|
-
await this.peerConnection.addIceCandidate(candidate);
|
|
358
|
-
}
|
|
359
|
-
this.pendingICECandidates = [];
|
|
360
|
-
}
|
|
361
|
-
async handleICECandidate(message) {
|
|
362
|
-
const decryptedCandidate = await EncryptionUtils.decryptMessage(message.payload, this.privateKey);
|
|
363
|
-
if (this.peerConnection.remoteDescription) {
|
|
364
|
-
await this.peerConnection.addIceCandidate(decryptedCandidate);
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
this.pendingICECandidates.push(decryptedCandidate);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
async handleDataMessage(message) {
|
|
371
|
-
const decryptedData = await EncryptionUtils.decryptMessage(message.payload, this.privateKey);
|
|
372
|
-
// Handle JSON-RPC request
|
|
373
|
-
const request = decryptedData;
|
|
374
|
-
if (request.method) {
|
|
375
|
-
console.log('Received JSON-RPC request:', request.method);
|
|
376
|
-
}
|
|
377
|
-
if ('result' in request) {
|
|
378
|
-
console.log('Received JSON-RPC response:', request.result);
|
|
379
|
-
}
|
|
380
|
-
if (!request.method && !('result' in request)) {
|
|
381
|
-
console.warn('Received unknown payload', request);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
// Notify message handlers
|
|
385
|
-
for (const handler of this.messageHandlers) {
|
|
386
|
-
try {
|
|
387
|
-
const result = await handler(request);
|
|
388
|
-
if (!result) {
|
|
389
|
-
console.log('Skipping response');
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
if ('result' in request) {
|
|
393
|
-
console.log('stopping echo');
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
// Send response back
|
|
397
|
-
const response = {
|
|
398
|
-
jsonrpc: '2.0',
|
|
399
|
-
id: request.id,
|
|
400
|
-
result,
|
|
401
|
-
};
|
|
402
|
-
await this.sendMessage(response);
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
// Send error response
|
|
406
|
-
const errorResponse = {
|
|
407
|
-
jsonrpc: '2.0',
|
|
408
|
-
id: request.id,
|
|
409
|
-
error: {
|
|
410
|
-
code: -32603,
|
|
411
|
-
message: 'Internal error',
|
|
412
|
-
data: error instanceof Error ? error.message : String(error),
|
|
413
|
-
},
|
|
414
|
-
};
|
|
415
|
-
await this.sendMessage(errorResponse);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
async sharePublicKey() {
|
|
420
|
-
if (!this.publicKey || !this.sessionId || this.hasSharedPublicKey)
|
|
421
|
-
return;
|
|
422
|
-
const publicKeyBase64 = await EncryptionUtils.publicKeyToBase64(this.publicKey);
|
|
423
|
-
const dAppInfo = {
|
|
424
|
-
name: 'Open Lavatory dApp',
|
|
425
|
-
url: typeof window !== 'undefined' ? window.location?.origin : 'unknown',
|
|
426
|
-
};
|
|
427
|
-
await this.signaling.publish({
|
|
428
|
-
type: 'pubkey',
|
|
429
|
-
payload: {
|
|
430
|
-
publicKey: publicKeyBase64,
|
|
431
|
-
dAppInfo,
|
|
432
|
-
},
|
|
433
|
-
sessionId: this.sessionId,
|
|
434
|
-
timestamp: Date.now(),
|
|
435
|
-
senderId: this.peerId,
|
|
436
|
-
});
|
|
437
|
-
this.hasSharedPublicKey = true;
|
|
438
|
-
}
|
|
439
|
-
async sendHello() {
|
|
440
|
-
if (!this.publicKey || !this.peerPublicKey)
|
|
441
|
-
return;
|
|
442
|
-
const publicKeyBase64 = await EncryptionUtils.publicKeyToBase64(this.publicKey);
|
|
443
|
-
const walletInfo = {
|
|
444
|
-
name: 'Open Lavatory Wallet',
|
|
445
|
-
version: '1.0.0',
|
|
446
|
-
};
|
|
447
|
-
const helloMessage = {
|
|
448
|
-
publicKey: publicKeyBase64,
|
|
449
|
-
walletInfo,
|
|
450
|
-
};
|
|
451
|
-
const encryptedHello = await EncryptionUtils.encryptMessage(helloMessage, this.peerPublicKey, this.privateKey, this.publicKey);
|
|
452
|
-
await this.signaling.publish({
|
|
453
|
-
type: 'hello',
|
|
454
|
-
payload: encryptedHello,
|
|
455
|
-
sessionId: this.sessionId,
|
|
456
|
-
timestamp: Date.now(),
|
|
457
|
-
senderId: this.peerId,
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
async setupWebRTC() {
|
|
461
|
-
const iceServers = [
|
|
462
|
-
{ urls: 'stun:stun.l.google.com:19302' },
|
|
463
|
-
{ urls: 'stun:stun1.l.google.com:19302' },
|
|
464
|
-
{ urls: 'stun:stun.services.mozilla.com:3478' },
|
|
465
|
-
{
|
|
466
|
-
urls: ['turn:openrelay.metered.ca:80', 'turn:openrelay.metered.ca:443'],
|
|
467
|
-
username: 'openrelayproject',
|
|
468
|
-
credential: 'openrelayproject',
|
|
469
|
-
},
|
|
470
|
-
];
|
|
471
|
-
this.peerConnection = new RTCPeerConnection({
|
|
472
|
-
iceServers,
|
|
473
|
-
iceCandidatePoolSize: 10,
|
|
474
|
-
iceTransportPolicy: 'all',
|
|
475
|
-
bundlePolicy: 'max-bundle',
|
|
476
|
-
rtcpMuxPolicy: 'require',
|
|
477
|
-
});
|
|
478
|
-
// Connection state handlers
|
|
479
|
-
this.peerConnection.onconnectionstatechange = () => {
|
|
480
|
-
const state = this.peerConnection.connectionState;
|
|
481
|
-
console.log('WebRTC connection state:', state);
|
|
482
|
-
if (state === 'connected') {
|
|
483
|
-
this.updateConnectionState('webrtc-connected', 'Direct P2P connection established');
|
|
484
|
-
}
|
|
485
|
-
else if (state === 'failed') {
|
|
486
|
-
this.retryWebRTC();
|
|
487
|
-
}
|
|
488
|
-
else if (state === 'connecting') {
|
|
489
|
-
this.updateConnectionState('webrtc-negotiating', 'Establishing P2P connection...');
|
|
490
|
-
}
|
|
491
|
-
};
|
|
492
|
-
// ICE candidate handler
|
|
493
|
-
this.peerConnection.onicecandidate = async (event) => {
|
|
494
|
-
if (event.candidate && this.peerPublicKey) {
|
|
495
|
-
const encryptedCandidate = await EncryptionUtils.encryptMessage(event.candidate, this.peerPublicKey, this.privateKey, this.publicKey);
|
|
496
|
-
await this.signaling.publish({
|
|
497
|
-
type: 'ice-candidate',
|
|
498
|
-
payload: encryptedCandidate,
|
|
499
|
-
sessionId: this.sessionId,
|
|
500
|
-
timestamp: Date.now(),
|
|
501
|
-
senderId: this.peerId,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
// Data channel setup
|
|
506
|
-
if (this.isInitiator) {
|
|
507
|
-
this.dataChannel = this.peerConnection.createDataChannel('openlv-data');
|
|
508
|
-
this.setupDataChannel();
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
this.peerConnection.ondatachannel = (event) => {
|
|
512
|
-
this.dataChannel = event.channel;
|
|
513
|
-
this.setupDataChannel();
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
setupDataChannel() {
|
|
518
|
-
if (!this.dataChannel)
|
|
519
|
-
return;
|
|
520
|
-
this.dataChannel.onopen = () => {
|
|
521
|
-
console.log('WebRTC data channel opened');
|
|
522
|
-
this.updateConnectionState('webrtc-connected', 'P2P data channel ready');
|
|
523
|
-
};
|
|
524
|
-
this.dataChannel.onmessage = async (event) => {
|
|
525
|
-
try {
|
|
526
|
-
const message = JSON.parse(event.data);
|
|
527
|
-
// Check if this is a request (has method) or response (has result/error)
|
|
528
|
-
if ('method' in message) {
|
|
529
|
-
// This is a request
|
|
530
|
-
const request = message;
|
|
531
|
-
console.log('Received P2P JSON-RPC request:', request.method);
|
|
532
|
-
// Handle the request - only send one response, not multiple
|
|
533
|
-
if (this.messageHandlers.length > 0) {
|
|
534
|
-
try {
|
|
535
|
-
// Use the first handler (there should typically be only one)
|
|
536
|
-
const result = await this.messageHandlers[0](request);
|
|
537
|
-
const response = {
|
|
538
|
-
jsonrpc: '2.0',
|
|
539
|
-
id: request.id,
|
|
540
|
-
result,
|
|
541
|
-
};
|
|
542
|
-
this.dataChannel.send(JSON.stringify(response));
|
|
543
|
-
}
|
|
544
|
-
catch (error) {
|
|
545
|
-
const errorResponse = {
|
|
546
|
-
jsonrpc: '2.0',
|
|
547
|
-
id: request.id,
|
|
548
|
-
error: {
|
|
549
|
-
code: -32603,
|
|
550
|
-
message: 'Internal error',
|
|
551
|
-
data: error instanceof Error ? error.message : String(error),
|
|
552
|
-
},
|
|
553
|
-
};
|
|
554
|
-
this.dataChannel.send(JSON.stringify(errorResponse));
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
// This is a response - just log it, don't process further
|
|
560
|
-
const response = message;
|
|
561
|
-
console.log('Received P2P JSON-RPC response for ID:', response.id, response);
|
|
562
|
-
// Responses are typically handled by the original sender, not processed here
|
|
563
|
-
// return response;
|
|
564
|
-
if (this.messageHandlers.length > 0) {
|
|
565
|
-
try {
|
|
566
|
-
await this.messageHandlers[0](response);
|
|
567
|
-
}
|
|
568
|
-
catch (error) {
|
|
569
|
-
console.error('Error handling response in message handler:', error);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
console.error('Error handling P2P message:', error);
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
this.dataChannel.onclose = () => {
|
|
579
|
-
this.updateConnectionState('mqtt-connected', 'P2P connection lost, using relay');
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
async createWebRTCOffer() {
|
|
583
|
-
if (!this.peerConnection || !this.peerPublicKey)
|
|
584
|
-
return;
|
|
585
|
-
const offer = await this.peerConnection.createOffer();
|
|
586
|
-
await this.peerConnection.setLocalDescription(offer);
|
|
587
|
-
const encryptedOffer = await EncryptionUtils.encryptMessage(offer, this.peerPublicKey, this.privateKey, this.publicKey);
|
|
588
|
-
await this.signaling.publish({
|
|
589
|
-
type: 'webrtc-offer',
|
|
590
|
-
payload: encryptedOffer,
|
|
591
|
-
sessionId: this.sessionId,
|
|
592
|
-
timestamp: Date.now(),
|
|
593
|
-
senderId: this.peerId,
|
|
594
|
-
});
|
|
595
|
-
this.setConnectionTimeout();
|
|
596
|
-
}
|
|
597
|
-
retryWebRTC() {
|
|
598
|
-
if (this.webrtcRetryCount >= this.maxWebrtcRetries) {
|
|
599
|
-
console.error('Max WebRTC retries reached, falling back to MQTT');
|
|
600
|
-
this.updateConnectionState('mqtt-connected', 'WebRTC failed, using relay fallback');
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
this.webrtcRetryCount++;
|
|
604
|
-
console.log(`Retrying WebRTC (${this.webrtcRetryCount}/${this.maxWebrtcRetries})`);
|
|
605
|
-
this.webrtcRetryInterval = setTimeout(async () => {
|
|
606
|
-
await this.setupWebRTC();
|
|
607
|
-
if (this.isInitiator) {
|
|
608
|
-
await this.createWebRTCOffer();
|
|
609
|
-
}
|
|
610
|
-
}, 2000 * this.webrtcRetryCount);
|
|
611
|
-
}
|
|
612
|
-
setConnectionTimeout() {
|
|
613
|
-
this.clearTimeouts();
|
|
614
|
-
this.connectionTimeout = setTimeout(() => {
|
|
615
|
-
console.warn('WebRTC connection timeout, retrying...');
|
|
616
|
-
this.retryWebRTC();
|
|
617
|
-
}, 15000);
|
|
618
|
-
}
|
|
619
|
-
clearTimeouts() {
|
|
620
|
-
if (this.webrtcRetryInterval) {
|
|
621
|
-
clearTimeout(this.webrtcRetryInterval);
|
|
622
|
-
this.webrtcRetryInterval = undefined;
|
|
623
|
-
}
|
|
624
|
-
if (this.connectionTimeout) {
|
|
625
|
-
clearTimeout(this.connectionTimeout);
|
|
626
|
-
this.connectionTimeout = undefined;
|
|
627
|
-
}
|
|
628
|
-
this.webrtcRetryCount = 0;
|
|
629
|
-
}
|
|
630
|
-
// Public API methods
|
|
631
|
-
async initSession() {
|
|
632
|
-
this.isInitiator = true;
|
|
633
|
-
this.sessionId = EncryptionUtils.generateSessionId();
|
|
634
|
-
// Generate keypair
|
|
635
|
-
const keyPair = await EncryptionUtils.generateKeyPair();
|
|
636
|
-
this.privateKey = keyPair.privateKey;
|
|
637
|
-
this.publicKey = keyPair.publicKey;
|
|
638
|
-
this.pubkeyHash = await EncryptionUtils.computePublicKeyHash(this.publicKey);
|
|
639
|
-
// Generate shared key for symmetric encryption during handshake
|
|
640
|
-
this.sharedKey = EncryptionUtils.generateSharedKey(); // Random shared key as hex
|
|
641
|
-
this.updateConnectionState('mqtt-connecting', 'Connecting to signaling server');
|
|
642
|
-
// Setup signaling
|
|
643
|
-
await this.signaling.waitForConnection();
|
|
644
|
-
await this.signaling.subscribe(this.sessionId, (message) => this.handleSignalingMessage(message));
|
|
645
|
-
this.updateConnectionState('pairing', 'Waiting for wallet to connect');
|
|
646
|
-
// Setup WebRTC
|
|
647
|
-
await this.setupWebRTC();
|
|
648
|
-
const openLVUrl = encodeConnectionURL({
|
|
649
|
-
sessionId: this.sessionId,
|
|
650
|
-
pubkeyHash: this.pubkeyHash,
|
|
651
|
-
sharedKey: this.sharedKey,
|
|
652
|
-
});
|
|
653
|
-
return { openLVUrl };
|
|
654
|
-
}
|
|
655
|
-
async connectToSession(openLVUrl) {
|
|
656
|
-
this.isInitiator = false;
|
|
657
|
-
const { sessionId, pubkeyHash, sharedKey } = decodeConnectionURL(openLVUrl);
|
|
658
|
-
this.sessionId = sessionId;
|
|
659
|
-
this.pubkeyHash = pubkeyHash;
|
|
660
|
-
this.sharedKey = sharedKey;
|
|
661
|
-
// Generate keypair
|
|
662
|
-
const keyPair = await EncryptionUtils.generateKeyPair();
|
|
663
|
-
this.privateKey = keyPair.privateKey;
|
|
664
|
-
this.publicKey = keyPair.publicKey;
|
|
665
|
-
this.updateConnectionState('mqtt-connecting', 'Connecting to signaling server');
|
|
666
|
-
// Setup signaling
|
|
667
|
-
await this.signaling.waitForConnection();
|
|
668
|
-
await this.signaling.subscribe(this.sessionId, (message) => this.handleSignalingMessage(message));
|
|
669
|
-
this.updateConnectionState('pairing', 'Requesting dApp public key');
|
|
670
|
-
// Setup WebRTC
|
|
671
|
-
await this.setupWebRTC();
|
|
672
|
-
// Send ping to request public key
|
|
673
|
-
await this.signaling.publish({
|
|
674
|
-
type: 'ping',
|
|
675
|
-
payload: 'request-pubkey',
|
|
676
|
-
sessionId: this.sessionId,
|
|
677
|
-
timestamp: Date.now(),
|
|
678
|
-
senderId: this.peerId,
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
async sendMessage(message) {
|
|
682
|
-
let jsonRpcMessage;
|
|
683
|
-
// If it's a string, wrap it in a custom JSON-RPC method
|
|
684
|
-
if (typeof message === 'string') {
|
|
685
|
-
jsonRpcMessage = {
|
|
686
|
-
jsonrpc: '2.0',
|
|
687
|
-
id: Date.now(),
|
|
688
|
-
method: 'lv_rawText',
|
|
689
|
-
params: [message],
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
jsonRpcMessage = message;
|
|
694
|
-
}
|
|
695
|
-
if (this.dataChannel && this.dataChannel.readyState === 'open') {
|
|
696
|
-
// Send via WebRTC
|
|
697
|
-
this.dataChannel.send(JSON.stringify(jsonRpcMessage));
|
|
698
|
-
}
|
|
699
|
-
else if (this.peerPublicKey && this.signaling.connected) {
|
|
700
|
-
// Send via encrypted MQTT
|
|
701
|
-
const encryptedMessage = await EncryptionUtils.encryptMessage(jsonRpcMessage, this.peerPublicKey, this.privateKey, this.publicKey);
|
|
702
|
-
await this.signaling.publish({
|
|
703
|
-
type: 'data',
|
|
704
|
-
payload: encryptedMessage,
|
|
705
|
-
sessionId: this.sessionId,
|
|
706
|
-
timestamp: Date.now(),
|
|
707
|
-
senderId: this.peerId,
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
throw new Error('No connection available to send message');
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
// Event handlers
|
|
715
|
-
onPhaseChange(handler) {
|
|
716
|
-
this.phaseHandlers.push(handler);
|
|
717
|
-
}
|
|
718
|
-
onMessage(handler) {
|
|
719
|
-
this.messageHandlers.push(handler);
|
|
720
|
-
}
|
|
721
|
-
onError(handler) {
|
|
722
|
-
this.errorHandlers.push(handler);
|
|
723
|
-
}
|
|
724
|
-
// Status getters
|
|
725
|
-
getConnectionStatus() {
|
|
726
|
-
switch (this.connectionState) {
|
|
727
|
-
case 'webrtc-connected':
|
|
728
|
-
return 'webrtc-connected';
|
|
729
|
-
case 'mqtt-connected':
|
|
730
|
-
case 'mqtt-connecting':
|
|
731
|
-
case 'pairing':
|
|
732
|
-
case 'key-exchange':
|
|
733
|
-
case 'webrtc-negotiating':
|
|
734
|
-
return 'mqtt-only';
|
|
735
|
-
default:
|
|
736
|
-
return 'disconnected';
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
getConnectionState() {
|
|
740
|
-
return this.connectionState;
|
|
741
|
-
}
|
|
742
|
-
disconnect() {
|
|
743
|
-
this.clearTimeouts();
|
|
744
|
-
if (this.dataChannel) {
|
|
745
|
-
this.dataChannel.close();
|
|
746
|
-
}
|
|
747
|
-
if (this.peerConnection) {
|
|
748
|
-
this.peerConnection.close();
|
|
749
|
-
}
|
|
750
|
-
this.signaling.disconnect();
|
|
751
|
-
this.updateConnectionState('disconnected', 'Connection closed');
|
|
752
|
-
// Reset state
|
|
753
|
-
this.hasSharedPublicKey = false;
|
|
754
|
-
this.pendingICECandidates = [];
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
//# sourceMappingURL=connection.js.map
|