@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.
- package/dist/abracadabra-provider.cjs +387 -13
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +386 -14
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +181 -3
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +17 -0
- package/src/AbracadabraProvider.ts +10 -1
- package/src/IdentityDoc.ts +533 -0
- package/src/index.ts +7 -0
- package/src/webrtc/DevicePairingChannel.ts +101 -13
- package/src/webrtc/SignalingSocket.ts +2 -2
|
@@ -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");
|
|
@@ -320,17 +391,34 @@ export class DevicePairingChannel extends EventEmitter {
|
|
|
320
391
|
},
|
|
321
392
|
);
|
|
322
393
|
|
|
323
|
-
//
|
|
324
|
-
this.
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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,
|