@abraca/dabra 1.0.21 → 1.0.23

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.
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { sha256 } from "@noble/hashes/sha256";
14
14
  import EventEmitter from "../EventEmitter.ts";
15
- import type { AbracadabraClient } from "../AbracadabraClient.ts";
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
- /** JWT token or async token factory for signaling auth. */
33
- token: string | (() => string) | (() => Promise<string>);
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: this.config.serverUrl,
288
- token: this.config.token,
289
- iceServers: this.config.iceServers,
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");
@@ -320,17 +391,34 @@ export class DevicePairingChannel extends EventEmitter {
320
391
  },
321
392
  );
322
393
 
323
- // Auto-destroy after timeout.
324
- this.timeoutHandle = setTimeout(() => {
325
- if (!this._destroyed) {
326
- this.emit("error", new Error("Pairing timed out"));
327
- this.destroy();
328
- }
329
- }, PAIRING_TIMEOUT_MS);
394
+ // If a fallback signaling server is configured, try it after a timeout.
395
+ if (this.config.fallbackSignalingUrl && !signalingUrl) {
396
+ const fallbackTimer = setTimeout(() => {
397
+ if (this._destroyed || connected) return;
398
+ // Primary server didn't connect in time — try fallback.
399
+ if (this.webrtc) {
400
+ this.webrtc.destroy();
401
+ this.webrtc = null;
402
+ }
403
+ this._usingFallback = true;
404
+ this.emit("fallback", { url: this.config.fallbackSignalingUrl });
405
+ this.connectToServer(
406
+ this.config.fallbackSignalingUrl!,
407
+ );
408
+ }, SIGNALING_CONNECT_TIMEOUT_MS);
409
+
410
+ // Clear fallback timer if primary succeeds.
411
+ this.webrtc.on("connected", () => clearTimeout(fallbackTimer));
412
+ }
330
413
 
331
414
  this.webrtc.connect();
332
415
  }
333
416
 
417
+ /** Whether the connection fell back to the fallback signaling server. */
418
+ get usingFallback(): boolean {
419
+ return this._usingFallback;
420
+ }
421
+
334
422
  private sendMessage(msg: PairingMsg): void {
335
423
  if (!this.webrtc || !this._connectedPeerId) return;
336
424
  // 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
- peerId: msg.peer_id,
242
- userId: msg.user_id,
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,