@abraca/dabra 1.0.22 → 1.0.24
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/abracadabra-provider.cjs +116 -22
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +116 -22
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +34 -3
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +17 -0
- package/src/IdentityDoc.ts +42 -6
- package/src/webrtc/AbracadabraWebRTC.ts +11 -5
- package/src/webrtc/DevicePairingChannel.ts +113 -13
- package/src/webrtc/SignalingSocket.ts +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -340,6 +340,11 @@ declare class AbracadabraClient {
|
|
|
340
340
|
publicKey: string;
|
|
341
341
|
};
|
|
342
342
|
}>;
|
|
343
|
+
/**
|
|
344
|
+
* Fetch a short-lived anonymous pairing token for WebRTC signaling.
|
|
345
|
+
* No authentication required. The token only grants access to `__pairing_*` rooms.
|
|
346
|
+
*/
|
|
347
|
+
static getPairingToken(serverUrl: string): Promise<string>;
|
|
343
348
|
/** Get encryption info for a document. */
|
|
344
349
|
getDocEncryption(docId: string): Promise<DocEncryptionInfo>;
|
|
345
350
|
/** Set the encryption mode for a document (no downgrade). */
|
|
@@ -2248,12 +2253,22 @@ declare class ManualSignaling extends EventEmitter {
|
|
|
2248
2253
|
interface DevicePairingConfig {
|
|
2249
2254
|
/** Server base URL (http/https). */
|
|
2250
2255
|
serverUrl: string;
|
|
2251
|
-
/**
|
|
2252
|
-
|
|
2256
|
+
/**
|
|
2257
|
+
* JWT token or async token factory for signaling auth.
|
|
2258
|
+
* When omitted, a short-lived anonymous pairing token is fetched automatically
|
|
2259
|
+
* from `POST /auth/pairing-token`.
|
|
2260
|
+
*/
|
|
2261
|
+
token?: string | (() => string) | (() => Promise<string>);
|
|
2253
2262
|
/** E2EE identity (Ed25519 public key + X25519 private key). */
|
|
2254
2263
|
e2ee: E2EEIdentity;
|
|
2255
2264
|
/** ICE servers. Defaults to Google STUN. */
|
|
2256
2265
|
iceServers?: RTCIceServer[];
|
|
2266
|
+
/**
|
|
2267
|
+
* Fallback signaling server URL. If the primary server is unreachable,
|
|
2268
|
+
* signaling will retry through this server. The actual pairing data is
|
|
2269
|
+
* E2EE-encrypted, so the relay server cannot read it.
|
|
2270
|
+
*/
|
|
2271
|
+
fallbackSignalingUrl?: string;
|
|
2257
2272
|
/** WebSocket polyfill (for Node.js). */
|
|
2258
2273
|
WebSocketPolyfill?: any;
|
|
2259
2274
|
}
|
|
@@ -2277,6 +2292,8 @@ declare class DevicePairingChannel extends EventEmitter {
|
|
|
2277
2292
|
private _destroyed;
|
|
2278
2293
|
private _pendingRequest;
|
|
2279
2294
|
private _connectedPeerId;
|
|
2295
|
+
private _usingFallback;
|
|
2296
|
+
private _resolvedIceServers;
|
|
2280
2297
|
readonly role: PairingRole;
|
|
2281
2298
|
readonly pairingCode: string;
|
|
2282
2299
|
private constructor();
|
|
@@ -2315,7 +2332,11 @@ declare class DevicePairingChannel extends EventEmitter {
|
|
|
2315
2332
|
requestPairing(request: PairingRequest): void;
|
|
2316
2333
|
get isDestroyed(): boolean;
|
|
2317
2334
|
destroy(): void;
|
|
2335
|
+
private resolveToken;
|
|
2318
2336
|
private start;
|
|
2337
|
+
private connectToServer;
|
|
2338
|
+
/** Whether the connection fell back to the fallback signaling server. */
|
|
2339
|
+
get usingFallback(): boolean;
|
|
2319
2340
|
private sendMessage;
|
|
2320
2341
|
private handleMessage;
|
|
2321
2342
|
}
|
|
@@ -2406,6 +2427,16 @@ interface IdentityDocConfiguration {
|
|
|
2406
2427
|
token?: string | (() => string) | (() => Promise<string>);
|
|
2407
2428
|
/** Per-server token factories keyed by base URL. */
|
|
2408
2429
|
tokens?: Record<string, string | (() => string) | (() => Promise<string>)>;
|
|
2430
|
+
/**
|
|
2431
|
+
* Crypto identity for Ed25519 challenge-response auth.
|
|
2432
|
+
* When provided, used instead of JWT tokens for authenticating with servers.
|
|
2433
|
+
*/
|
|
2434
|
+
cryptoIdentity?: CryptoIdentity | (() => Promise<CryptoIdentity>);
|
|
2435
|
+
/**
|
|
2436
|
+
* Signs a base64url challenge and returns a base64url signature.
|
|
2437
|
+
* Required when cryptoIdentity is set.
|
|
2438
|
+
*/
|
|
2439
|
+
signChallenge?: (challenge: string) => Promise<string>;
|
|
2409
2440
|
/**
|
|
2410
2441
|
* WebRTC configuration for P2P identity sync.
|
|
2411
2442
|
* When provided, enables E2EE peer-to-peer sync using signaling from
|
|
@@ -2444,7 +2475,7 @@ declare class IdentityDocProvider extends EventEmitter {
|
|
|
2444
2475
|
private _destroyed;
|
|
2445
2476
|
constructor(configuration: IdentityDocConfiguration);
|
|
2446
2477
|
connect(): void;
|
|
2447
|
-
|
|
2478
|
+
connectToServer(key: string, serverUrl: string): void;
|
|
2448
2479
|
private _connectWebRTC;
|
|
2449
2480
|
get profileMap(): Y.Map<string>;
|
|
2450
2481
|
get serversMap(): Y.Map<Y.Map<any>>;
|
package/package.json
CHANGED
package/src/AbracadabraClient.ts
CHANGED
|
@@ -227,6 +227,23 @@ export class AbracadabraClient {
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ── Pairing ─────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fetch a short-lived anonymous pairing token for WebRTC signaling.
|
|
234
|
+
* No authentication required. The token only grants access to `__pairing_*` rooms.
|
|
235
|
+
*/
|
|
236
|
+
static async getPairingToken(serverUrl: string): Promise<string> {
|
|
237
|
+
let base = serverUrl;
|
|
238
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
239
|
+
const resp = await fetch(`${base}/auth/pairing-token`, { method: "POST" });
|
|
240
|
+
if (!resp.ok) {
|
|
241
|
+
throw new Error(`Failed to fetch pairing token: ${resp.status}`);
|
|
242
|
+
}
|
|
243
|
+
const { token } = (await resp.json()) as { token: string };
|
|
244
|
+
return token;
|
|
245
|
+
}
|
|
246
|
+
|
|
230
247
|
// ── Encryption ───────────────────────────────────────────────────────────
|
|
231
248
|
|
|
232
249
|
/** Get encryption info for a document. */
|
package/src/IdentityDoc.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AbracadabraProvider } from "./AbracadabraProvider.ts";
|
|
|
5
5
|
import type { AbracadabraProviderConfiguration } from "./AbracadabraProvider.ts";
|
|
6
6
|
import { AbracadabraWS } from "./AbracadabraWS.ts";
|
|
7
7
|
import { AbracadabraWebRTC } from "./webrtc/AbracadabraWebRTC.ts";
|
|
8
|
+
import type { CryptoIdentity } from "./types.ts";
|
|
8
9
|
import type { E2EEIdentity } from "./webrtc/E2EEChannel.ts";
|
|
9
10
|
|
|
10
11
|
// ── Identity Doc ID Derivation ─────────────────────────────────────────────
|
|
@@ -89,6 +90,18 @@ export interface IdentityDocConfiguration {
|
|
|
89
90
|
/** Per-server token factories keyed by base URL. */
|
|
90
91
|
tokens?: Record<string, string | (() => string) | (() => Promise<string>)>;
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Crypto identity for Ed25519 challenge-response auth.
|
|
95
|
+
* When provided, used instead of JWT tokens for authenticating with servers.
|
|
96
|
+
*/
|
|
97
|
+
cryptoIdentity?: CryptoIdentity | (() => Promise<CryptoIdentity>);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Signs a base64url challenge and returns a base64url signature.
|
|
101
|
+
* Required when cryptoIdentity is set.
|
|
102
|
+
*/
|
|
103
|
+
signChallenge?: (challenge: string) => Promise<string>;
|
|
104
|
+
|
|
92
105
|
/**
|
|
93
106
|
* WebRTC configuration for P2P identity sync.
|
|
94
107
|
* When provided, enables E2EE peer-to-peer sync using signaling from
|
|
@@ -164,7 +177,7 @@ export class IdentityDocProvider extends EventEmitter {
|
|
|
164
177
|
|
|
165
178
|
for (const { url, key } of targets) {
|
|
166
179
|
if (this.providers.has(key)) continue;
|
|
167
|
-
this.
|
|
180
|
+
this.connectToServer(key, url);
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
if (this.config.webrtc && !this.webrtc) {
|
|
@@ -172,7 +185,21 @@ export class IdentityDocProvider extends EventEmitter {
|
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
|
|
175
|
-
|
|
188
|
+
connectToServer(key: string, serverUrl: string): void {
|
|
189
|
+
if (this._destroyed) return;
|
|
190
|
+
|
|
191
|
+
// Tear down existing connection for this key (idempotent reconnect)
|
|
192
|
+
const existingProvider = this.providers.get(key);
|
|
193
|
+
if (existingProvider) {
|
|
194
|
+
existingProvider.destroy();
|
|
195
|
+
this.providers.delete(key);
|
|
196
|
+
}
|
|
197
|
+
const existingWs = this.websockets.get(key);
|
|
198
|
+
if (existingWs) {
|
|
199
|
+
existingWs.destroy();
|
|
200
|
+
this.websockets.delete(key);
|
|
201
|
+
}
|
|
202
|
+
|
|
176
203
|
const token =
|
|
177
204
|
this.config.tokens?.[serverUrl] ?? this.config.token ?? "";
|
|
178
205
|
|
|
@@ -184,17 +211,26 @@ export class IdentityDocProvider extends EventEmitter {
|
|
|
184
211
|
const ws = new AbracadabraWS({ url: wsUrl, WebSocketPolyfill: undefined as any });
|
|
185
212
|
this.websockets.set(key, ws);
|
|
186
213
|
|
|
187
|
-
const
|
|
214
|
+
const providerConfig: AbracadabraProviderConfiguration = {
|
|
188
215
|
name: this.docId,
|
|
189
216
|
document: this.document,
|
|
190
217
|
websocketProvider: ws,
|
|
191
|
-
token,
|
|
192
218
|
serverAgnostic: true,
|
|
193
219
|
disableOfflineStore: key !== "local" && key !== "sync"
|
|
194
220
|
? true
|
|
195
221
|
: this.config.disableOfflineStore ?? false,
|
|
196
222
|
...this.config.providerDefaults,
|
|
197
|
-
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Use crypto identity auth if available, otherwise JWT token
|
|
226
|
+
if (this.config.cryptoIdentity && this.config.signChallenge) {
|
|
227
|
+
providerConfig.cryptoIdentity = this.config.cryptoIdentity;
|
|
228
|
+
providerConfig.signChallenge = this.config.signChallenge;
|
|
229
|
+
} else {
|
|
230
|
+
providerConfig.token = token;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const provider = new AbracadabraProvider(providerConfig);
|
|
198
234
|
|
|
199
235
|
provider.on("synced", () => this.emit("synced", { server: key }));
|
|
200
236
|
provider.on("status", (data: any) =>
|
|
@@ -467,7 +503,7 @@ export class IdentityDocProvider extends EventEmitter {
|
|
|
467
503
|
|
|
468
504
|
if (url) {
|
|
469
505
|
this.config = { ...this.config, syncServerUrl: url };
|
|
470
|
-
this.
|
|
506
|
+
this.connectToServer("sync", url);
|
|
471
507
|
}
|
|
472
508
|
}
|
|
473
509
|
|
|
@@ -349,7 +349,7 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
349
349
|
// Wait for open before sending.
|
|
350
350
|
channel.onopen = () => {
|
|
351
351
|
const data = new TextEncoder().encode(payload);
|
|
352
|
-
pc.router.send(channelName, data);
|
|
352
|
+
pc.router.send(channelName, data).catch(() => {});
|
|
353
353
|
};
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
@@ -475,8 +475,12 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
475
475
|
// Listen for key-exchange messages on the router.
|
|
476
476
|
pc.router.on("channelMessage", async ({ name, data }: { name: string; data: any }) => {
|
|
477
477
|
if (name === KEY_EXCHANGE_CHANNEL) {
|
|
478
|
-
|
|
479
|
-
|
|
478
|
+
try {
|
|
479
|
+
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
480
|
+
await e2ee.handleKeyExchange(buf);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
this.emit("e2eeFailed", { peerId, error: err });
|
|
483
|
+
}
|
|
480
484
|
}
|
|
481
485
|
});
|
|
482
486
|
|
|
@@ -559,7 +563,8 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
559
563
|
try {
|
|
560
564
|
const sdp = await pc.createOffer();
|
|
561
565
|
this.signaling?.sendOffer(peerId, sdp);
|
|
562
|
-
} catch {
|
|
566
|
+
} catch (err) {
|
|
567
|
+
this.emit("error", { type: "offer-failed", peerId, error: err });
|
|
563
568
|
this.removePeer(peerId);
|
|
564
569
|
}
|
|
565
570
|
}
|
|
@@ -581,7 +586,8 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
581
586
|
try {
|
|
582
587
|
const answerSdp = await pc.setRemoteOffer(sdp);
|
|
583
588
|
this.signaling?.sendAnswer(from, answerSdp);
|
|
584
|
-
} catch {
|
|
589
|
+
} catch (err) {
|
|
590
|
+
this.emit("error", { type: "answer-failed", peerId: from, error: err });
|
|
585
591
|
this.removePeer(from);
|
|
586
592
|
}
|
|
587
593
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { sha256 } from "@noble/hashes/sha256";
|
|
14
14
|
import EventEmitter from "../EventEmitter.ts";
|
|
15
|
-
import
|
|
15
|
+
import { AbracadabraClient } from "../AbracadabraClient.ts";
|
|
16
16
|
import { AbracadabraWebRTC } from "./AbracadabraWebRTC.ts";
|
|
17
17
|
import type { E2EEIdentity } from "./E2EEChannel.ts";
|
|
18
18
|
|
|
@@ -22,6 +22,7 @@ import type { E2EEIdentity } from "./E2EEChannel.ts";
|
|
|
22
22
|
const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
23
23
|
const CODE_LENGTH = 6;
|
|
24
24
|
const PAIRING_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
|
+
const SIGNALING_CONNECT_TIMEOUT_MS = 5_000; // 5 seconds before trying fallback
|
|
25
26
|
const PAIRING_CHANNEL = "device-pairing";
|
|
26
27
|
|
|
27
28
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -29,12 +30,22 @@ const PAIRING_CHANNEL = "device-pairing";
|
|
|
29
30
|
export interface DevicePairingConfig {
|
|
30
31
|
/** Server base URL (http/https). */
|
|
31
32
|
serverUrl: string;
|
|
32
|
-
/**
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* JWT token or async token factory for signaling auth.
|
|
35
|
+
* When omitted, a short-lived anonymous pairing token is fetched automatically
|
|
36
|
+
* from `POST /auth/pairing-token`.
|
|
37
|
+
*/
|
|
38
|
+
token?: string | (() => string) | (() => Promise<string>);
|
|
34
39
|
/** E2EE identity (Ed25519 public key + X25519 private key). */
|
|
35
40
|
e2ee: E2EEIdentity;
|
|
36
41
|
/** ICE servers. Defaults to Google STUN. */
|
|
37
42
|
iceServers?: RTCIceServer[];
|
|
43
|
+
/**
|
|
44
|
+
* Fallback signaling server URL. If the primary server is unreachable,
|
|
45
|
+
* signaling will retry through this server. The actual pairing data is
|
|
46
|
+
* E2EE-encrypted, so the relay server cannot read it.
|
|
47
|
+
*/
|
|
48
|
+
fallbackSignalingUrl?: string;
|
|
38
49
|
/** WebSocket polyfill (for Node.js). */
|
|
39
50
|
WebSocketPolyfill?: any;
|
|
40
51
|
}
|
|
@@ -106,6 +117,8 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
106
117
|
private _destroyed = false;
|
|
107
118
|
private _pendingRequest: PairingRequest | null = null;
|
|
108
119
|
private _connectedPeerId: string | null = null;
|
|
120
|
+
private _usingFallback = false;
|
|
121
|
+
private _resolvedIceServers: RTCIceServer[] | undefined;
|
|
109
122
|
|
|
110
123
|
readonly role: PairingRole;
|
|
111
124
|
readonly pairingCode: string;
|
|
@@ -279,14 +292,66 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
279
292
|
|
|
280
293
|
// ── Private ─────────────────────────────────────────────────────────
|
|
281
294
|
|
|
295
|
+
private async resolveToken(
|
|
296
|
+
serverUrl?: string,
|
|
297
|
+
): Promise<string | (() => string) | (() => Promise<string>)> {
|
|
298
|
+
// Use provided token if connecting to the primary server.
|
|
299
|
+
if (this.config.token && serverUrl === this.config.serverUrl) {
|
|
300
|
+
return this.config.token;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fetch a short-lived anonymous pairing token from the target server.
|
|
304
|
+
let base = serverUrl ?? this.config.serverUrl;
|
|
305
|
+
while (base.endsWith("/")) base = base.slice(0, -1);
|
|
306
|
+
const resp = await fetch(`${base}/auth/pairing-token`, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
});
|
|
309
|
+
if (!resp.ok) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Failed to fetch pairing token: ${resp.status} ${resp.statusText}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const { token } = (await resp.json()) as { token: string };
|
|
315
|
+
return token;
|
|
316
|
+
}
|
|
317
|
+
|
|
282
318
|
private start(): void {
|
|
319
|
+
this.connectToServer(this.config.serverUrl);
|
|
320
|
+
|
|
321
|
+
// Auto-destroy after timeout.
|
|
322
|
+
this.timeoutHandle = setTimeout(() => {
|
|
323
|
+
if (!this._destroyed) {
|
|
324
|
+
this.emit("error", new Error("Pairing timed out"));
|
|
325
|
+
this.destroy();
|
|
326
|
+
}
|
|
327
|
+
}, PAIRING_TIMEOUT_MS);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private connectToServer(serverUrl: string, signalingUrl?: string): void {
|
|
283
331
|
const roomId = codeToRoomId(this.pairingCode);
|
|
284
332
|
|
|
333
|
+
// Resolve token (possibly async) — uses a factory so SignalingSocket can await it.
|
|
334
|
+
const tokenPromise = this.resolveToken(serverUrl);
|
|
335
|
+
const tokenFactory = async (): Promise<string> => {
|
|
336
|
+
const t = await tokenPromise;
|
|
337
|
+
if (typeof t === "function") return await t();
|
|
338
|
+
return t;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Fetch ICE servers from the target server if none configured.
|
|
342
|
+
if (!this.config.iceServers) {
|
|
343
|
+
const client = new AbracadabraClient({ url: serverUrl });
|
|
344
|
+
client.getIceServers().then((servers) => {
|
|
345
|
+
if (servers.length > 0) this._resolvedIceServers = servers;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
285
349
|
this.webrtc = new AbracadabraWebRTC({
|
|
286
350
|
docId: roomId,
|
|
287
|
-
url:
|
|
288
|
-
|
|
289
|
-
|
|
351
|
+
url: serverUrl,
|
|
352
|
+
signalingUrl: signalingUrl ?? undefined,
|
|
353
|
+
token: tokenFactory,
|
|
354
|
+
iceServers: this.config.iceServers ?? this._resolvedIceServers,
|
|
290
355
|
e2ee: this.config.e2ee,
|
|
291
356
|
enableDocSync: false,
|
|
292
357
|
enableAwarenessSync: false,
|
|
@@ -295,6 +360,12 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
295
360
|
WebSocketPolyfill: this.config.WebSocketPolyfill,
|
|
296
361
|
});
|
|
297
362
|
|
|
363
|
+
let connected = false;
|
|
364
|
+
|
|
365
|
+
this.webrtc.on("connected", () => {
|
|
366
|
+
connected = true;
|
|
367
|
+
});
|
|
368
|
+
|
|
298
369
|
this.webrtc.on("e2eeEstablished", ({ peerId }: { peerId: string }) => {
|
|
299
370
|
this._connectedPeerId = peerId;
|
|
300
371
|
this.emit("connected");
|
|
@@ -307,6 +378,18 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
307
378
|
},
|
|
308
379
|
);
|
|
309
380
|
|
|
381
|
+
this.webrtc.on("e2eeFailed", ({ peerId, error }: { peerId: string; error: any }) => {
|
|
382
|
+
if (!this._destroyed) {
|
|
383
|
+
this.emit("error", new Error(`E2EE failed: ${error?.message ?? error}`));
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
this.webrtc.on("error", (err: any) => {
|
|
388
|
+
if (!this._destroyed) {
|
|
389
|
+
this.emit("error", new Error(`WebRTC: ${err?.type ?? err?.message ?? "unknown"}`));
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
310
393
|
this.webrtc.on("peerLeft", () => {
|
|
311
394
|
if (!this._destroyed) {
|
|
312
395
|
this.emit("error", new Error("Peer disconnected"));
|
|
@@ -320,17 +403,34 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
320
403
|
},
|
|
321
404
|
);
|
|
322
405
|
|
|
323
|
-
//
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
406
|
+
// If a fallback signaling server is configured, try it after a timeout.
|
|
407
|
+
if (this.config.fallbackSignalingUrl && !signalingUrl) {
|
|
408
|
+
const fallbackTimer = setTimeout(() => {
|
|
409
|
+
if (this._destroyed || connected) return;
|
|
410
|
+
// Primary server didn't connect in time — try fallback.
|
|
411
|
+
if (this.webrtc) {
|
|
412
|
+
this.webrtc.destroy();
|
|
413
|
+
this.webrtc = null;
|
|
414
|
+
}
|
|
415
|
+
this._usingFallback = true;
|
|
416
|
+
this.emit("fallback", { url: this.config.fallbackSignalingUrl });
|
|
417
|
+
this.connectToServer(
|
|
418
|
+
this.config.fallbackSignalingUrl!,
|
|
419
|
+
);
|
|
420
|
+
}, SIGNALING_CONNECT_TIMEOUT_MS);
|
|
421
|
+
|
|
422
|
+
// Clear fallback timer if primary succeeds.
|
|
423
|
+
this.webrtc.on("connected", () => clearTimeout(fallbackTimer));
|
|
424
|
+
}
|
|
330
425
|
|
|
331
426
|
this.webrtc.connect();
|
|
332
427
|
}
|
|
333
428
|
|
|
429
|
+
/** Whether the connection fell back to the fallback signaling server. */
|
|
430
|
+
get usingFallback(): boolean {
|
|
431
|
+
return this._usingFallback;
|
|
432
|
+
}
|
|
433
|
+
|
|
334
434
|
private sendMessage(msg: PairingMsg): void {
|
|
335
435
|
if (!this.webrtc || !this._connectedPeerId) return;
|
|
336
436
|
// Send via the custom message path — the E2EE layer on the data channel
|
|
@@ -238,8 +238,8 @@ export class SignalingSocket extends EventEmitter {
|
|
|
238
238
|
|
|
239
239
|
case "joined":
|
|
240
240
|
this.emit("joined", {
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
peer_id: msg.peer_id,
|
|
242
|
+
user_id: msg.user_id,
|
|
243
243
|
muted: msg.muted,
|
|
244
244
|
video: msg.video,
|
|
245
245
|
screen: msg.screen,
|