@abraca/dabra 1.2.0 → 1.3.1
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 +119 -17
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +119 -17
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +35 -3
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +37 -0
- package/src/CryptoIdentityKeystore.ts +23 -0
- package/src/IdentityDoc.ts +2 -2
- package/src/webrtc/AbracadabraWebRTC.ts +79 -25
- package/src/webrtc/types.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -340,6 +340,28 @@ declare class AbracadabraClient {
|
|
|
340
340
|
publicKey: string;
|
|
341
341
|
};
|
|
342
342
|
}>;
|
|
343
|
+
/** Request a device session token after successful crypto auth. Requires valid JWT. */
|
|
344
|
+
requestDeviceSession(opts: {
|
|
345
|
+
publicKey: string;
|
|
346
|
+
deviceName?: string;
|
|
347
|
+
}): Promise<{
|
|
348
|
+
sessionId: string;
|
|
349
|
+
sessionToken: string;
|
|
350
|
+
expiresAt: number;
|
|
351
|
+
}>;
|
|
352
|
+
/** Exchange a device session token for a fresh JWT. No biometric/passkey needed. */
|
|
353
|
+
refreshWithDeviceSession(sessionToken: string): Promise<string>;
|
|
354
|
+
/** List active device sessions for the authenticated user. */
|
|
355
|
+
listDeviceSessions(): Promise<Array<{
|
|
356
|
+
id: string;
|
|
357
|
+
keyId: string;
|
|
358
|
+
deviceName?: string;
|
|
359
|
+
issuedAt: number;
|
|
360
|
+
expiresAt: number;
|
|
361
|
+
lastUsedAt?: number;
|
|
362
|
+
}>>;
|
|
363
|
+
/** Revoke a device session by ID. */
|
|
364
|
+
revokeDeviceSession(sessionId: string): Promise<void>;
|
|
343
365
|
/**
|
|
344
366
|
* Fetch a short-lived anonymous pairing token for WebRTC signaling.
|
|
345
367
|
* No authentication required. The token only grants access to `__pairing_*` rooms.
|
|
@@ -584,6 +606,11 @@ declare class CryptoIdentityKeystore {
|
|
|
584
606
|
* Returns the locally-cached username label, or null if no identity is cached.
|
|
585
607
|
*/
|
|
586
608
|
getUsername(credentialIdHint?: string): Promise<string | null>;
|
|
609
|
+
/**
|
|
610
|
+
* Updates the cached username for a given credential (or the first cached identity).
|
|
611
|
+
* Call this after the user sets/changes their display name so it persists across devices.
|
|
612
|
+
*/
|
|
613
|
+
setUsername(username: string, credentialIdHint?: string): Promise<void>;
|
|
587
614
|
/** Returns true if an identity is cached in IndexedDB. */
|
|
588
615
|
hasIdentity(): Promise<boolean>;
|
|
589
616
|
/** Remove cached identity record(s) from IndexedDB. The passkey itself
|
|
@@ -2060,7 +2087,7 @@ interface AbracadabraWebRTCConfiguration {
|
|
|
2060
2087
|
* When provided, all data channel messages (except key-exchange) are
|
|
2061
2088
|
* encrypted with AES-256-GCM using X25519 ECDH-derived session keys.
|
|
2062
2089
|
*/
|
|
2063
|
-
e2ee?: E2EEIdentity;
|
|
2090
|
+
e2ee?: E2EEIdentity | (() => Promise<E2EEIdentity>);
|
|
2064
2091
|
/** WebSocket polyfill for signaling (e.g. for Node.js). */
|
|
2065
2092
|
WebSocketPolyfill?: any;
|
|
2066
2093
|
}
|
|
@@ -2121,6 +2148,9 @@ declare class AbracadabraWebRTC extends EventEmitter {
|
|
|
2121
2148
|
private fileChannels;
|
|
2122
2149
|
private e2eeChannels;
|
|
2123
2150
|
private readonly config;
|
|
2151
|
+
/** Cached resolved E2EE identity (lazily resolved from factory on first peer connect). */
|
|
2152
|
+
private _resolvedE2ee;
|
|
2153
|
+
private _resolveE2eePromise;
|
|
2124
2154
|
readonly peers: Map<string, PeerState>;
|
|
2125
2155
|
localPeerId: string | null;
|
|
2126
2156
|
isConnected: boolean;
|
|
@@ -2157,6 +2187,8 @@ declare class AbracadabraWebRTC extends EventEmitter {
|
|
|
2157
2187
|
private removePeer;
|
|
2158
2188
|
private removeAllPeers;
|
|
2159
2189
|
private createPeerConnection;
|
|
2190
|
+
/** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
|
|
2191
|
+
private resolveE2ee;
|
|
2160
2192
|
private attachDataHandlers;
|
|
2161
2193
|
private startDataSync;
|
|
2162
2194
|
private initiateConnection;
|
|
@@ -2530,8 +2562,8 @@ interface IdentityDocConfiguration {
|
|
|
2530
2562
|
*/
|
|
2531
2563
|
webrtc?: {
|
|
2532
2564
|
/** Server URL to use for signaling (any connected server works). */signalingServerUrl: string; /** Token for the signaling server. */
|
|
2533
|
-
token: string | (() => string) | (() => Promise<string>); /** E2EE identity for the data channel. */
|
|
2534
|
-
e2ee?: E2EEIdentity; /** ICE servers. */
|
|
2565
|
+
token: string | (() => string) | (() => Promise<string>); /** E2EE identity for the data channel. Accepts a factory for lazy derivation. */
|
|
2566
|
+
e2ee?: E2EEIdentity | (() => Promise<E2EEIdentity>); /** ICE servers. */
|
|
2535
2567
|
iceServers?: RTCIceServer[];
|
|
2536
2568
|
};
|
|
2537
2569
|
/** Disable IndexedDB offline store. */
|
package/package.json
CHANGED
package/src/AbracadabraClient.ts
CHANGED
|
@@ -227,6 +227,43 @@ export class AbracadabraClient {
|
|
|
227
227
|
});
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// ── Device Sessions ────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/** Request a device session token after successful crypto auth. Requires valid JWT. */
|
|
233
|
+
async requestDeviceSession(opts: {
|
|
234
|
+
publicKey: string;
|
|
235
|
+
deviceName?: string;
|
|
236
|
+
}): Promise<{ sessionId: string; sessionToken: string; expiresAt: number }> {
|
|
237
|
+
return this.request("POST", "/auth/device-session", {
|
|
238
|
+
body: { publicKey: opts.publicKey, deviceName: opts.deviceName },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Exchange a device session token for a fresh JWT. No biometric/passkey needed. */
|
|
243
|
+
async refreshWithDeviceSession(sessionToken: string): Promise<string> {
|
|
244
|
+
const res = await this.request<{ token: string }>("POST", "/auth/refresh", {
|
|
245
|
+
body: { sessionToken },
|
|
246
|
+
auth: false,
|
|
247
|
+
});
|
|
248
|
+
this.token = res.token;
|
|
249
|
+
return res.token;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** List active device sessions for the authenticated user. */
|
|
253
|
+
async listDeviceSessions(): Promise<
|
|
254
|
+
Array<{ id: string; keyId: string; deviceName?: string; issuedAt: number; expiresAt: number; lastUsedAt?: number }>
|
|
255
|
+
> {
|
|
256
|
+
const res = await this.request<{
|
|
257
|
+
sessions: Array<{ id: string; keyId: string; deviceName?: string; issuedAt: number; expiresAt: number; lastUsedAt?: number }>;
|
|
258
|
+
}>("GET", "/auth/device-session");
|
|
259
|
+
return res.sessions;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Revoke a device session by ID. */
|
|
263
|
+
async revokeDeviceSession(sessionId: string): Promise<void> {
|
|
264
|
+
await this.request("DELETE", `/auth/device-session/${encodeURIComponent(sessionId)}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
230
267
|
// ── Pairing ─────────────────────────────────────────────────────────────
|
|
231
268
|
|
|
232
269
|
/**
|
|
@@ -322,6 +322,29 @@ export class CryptoIdentityKeystore {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Updates the cached username for a given credential (or the first cached identity).
|
|
327
|
+
* Call this after the user sets/changes their display name so it persists across devices.
|
|
328
|
+
*/
|
|
329
|
+
async setUsername(username: string, credentialIdHint?: string): Promise<void> {
|
|
330
|
+
const db = await openDb();
|
|
331
|
+
try {
|
|
332
|
+
if (credentialIdHint) {
|
|
333
|
+
const stored = await dbGet(db, credentialIdHint);
|
|
334
|
+
if (stored) {
|
|
335
|
+
await dbPut(db, credentialIdHint, { ...stored, username });
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
const all = await dbGetAll(db);
|
|
339
|
+
if (all.length > 0) {
|
|
340
|
+
await dbPut(db, all[0].key, { ...all[0].value, username });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} finally {
|
|
344
|
+
db.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
325
348
|
/** Returns true if an identity is cached in IndexedDB. */
|
|
326
349
|
async hasIdentity(): Promise<boolean> {
|
|
327
350
|
const db = await openDb();
|
package/src/IdentityDoc.ts
CHANGED
|
@@ -132,8 +132,8 @@ export interface IdentityDocConfiguration {
|
|
|
132
132
|
signalingServerUrl: string;
|
|
133
133
|
/** Token for the signaling server. */
|
|
134
134
|
token: string | (() => string) | (() => Promise<string>);
|
|
135
|
-
/** E2EE identity for the data channel. */
|
|
136
|
-
e2ee?: E2EEIdentity;
|
|
135
|
+
/** E2EE identity for the data channel. Accepts a factory for lazy derivation. */
|
|
136
|
+
e2ee?: E2EEIdentity | (() => Promise<E2EEIdentity>);
|
|
137
137
|
/** ICE servers. */
|
|
138
138
|
iceServers?: RTCIceServer[];
|
|
139
139
|
};
|
|
@@ -49,10 +49,14 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
49
49
|
enableAwarenessSync: boolean;
|
|
50
50
|
enableFileTransfer: boolean;
|
|
51
51
|
fileChunkSize: number;
|
|
52
|
-
e2ee: E2EEIdentity | null;
|
|
52
|
+
e2ee: E2EEIdentity | (() => Promise<E2EEIdentity>) | null;
|
|
53
53
|
WebSocketPolyfill: any;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
+
/** Cached resolved E2EE identity (lazily resolved from factory on first peer connect). */
|
|
57
|
+
private _resolvedE2ee: E2EEIdentity | null = null;
|
|
58
|
+
private _resolveE2eePromise: Promise<E2EEIdentity> | null = null;
|
|
59
|
+
|
|
56
60
|
public readonly peers = new Map<string, PeerState>();
|
|
57
61
|
public localPeerId: string | null = null;
|
|
58
62
|
public isConnected = false;
|
|
@@ -284,6 +288,9 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
284
288
|
this.signaling = null;
|
|
285
289
|
}
|
|
286
290
|
|
|
291
|
+
this._resolvedE2ee = null;
|
|
292
|
+
this._resolveE2eePromise = null;
|
|
293
|
+
|
|
287
294
|
this.removeAllListeners();
|
|
288
295
|
}
|
|
289
296
|
|
|
@@ -471,45 +478,92 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
471
478
|
return pc;
|
|
472
479
|
}
|
|
473
480
|
|
|
481
|
+
/** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
|
|
482
|
+
private async resolveE2ee(): Promise<E2EEIdentity | null> {
|
|
483
|
+
if (this._resolvedE2ee) return this._resolvedE2ee;
|
|
484
|
+
if (!this.config.e2ee) return null;
|
|
485
|
+
if (typeof this.config.e2ee === "function") {
|
|
486
|
+
if (!this._resolveE2eePromise) {
|
|
487
|
+
this._resolveE2eePromise = this.config.e2ee().then((id) => {
|
|
488
|
+
this._resolvedE2ee = id;
|
|
489
|
+
return id;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return this._resolveE2eePromise;
|
|
493
|
+
}
|
|
494
|
+
this._resolvedE2ee = this.config.e2ee;
|
|
495
|
+
return this._resolvedE2ee;
|
|
496
|
+
}
|
|
497
|
+
|
|
474
498
|
private attachDataHandlers(peerId: string, pc: PeerConnection): void {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
499
|
+
if (!this.config.e2ee) {
|
|
500
|
+
this.startDataSync(peerId, pc);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// E2EE identity may resolve asynchronously (e.g. lazy passkey-derived key).
|
|
505
|
+
// Register handlers synchronously so no messages are lost during resolution.
|
|
506
|
+
let e2ee: E2EEChannel | null = null;
|
|
507
|
+
const pendingMessages: Uint8Array[] = [];
|
|
508
|
+
let pendingKeyExchangeChannel: RTCDataChannel | null = null;
|
|
509
|
+
|
|
510
|
+
pc.router.on("channelMessage", async ({ name, data }: { name: string; data: any }) => {
|
|
511
|
+
if (name !== KEY_EXCHANGE_CHANNEL) return;
|
|
512
|
+
const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
513
|
+
if (!e2ee) {
|
|
514
|
+
pendingMessages.push(buf);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
await e2ee.handleKeyExchange(buf);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
this.emit("e2eeFailed", { peerId, error: err });
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
pc.router.on("channelOpen", ({ name, channel }: { name: string; channel: RTCDataChannel }) => {
|
|
525
|
+
if (name !== KEY_EXCHANGE_CHANNEL) return;
|
|
526
|
+
if (!e2ee) {
|
|
527
|
+
pendingKeyExchangeChannel = channel;
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
channel.send(e2ee.getKeyExchangeMessage());
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
this.resolveE2ee().then(async (identity) => {
|
|
534
|
+
if (!identity) {
|
|
535
|
+
this.startDataSync(peerId, pc);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
e2ee = new E2EEChannel(identity, this.config.docId);
|
|
478
539
|
this.e2eeChannels.set(peerId, e2ee);
|
|
479
540
|
pc.router.setEncryptor(e2ee);
|
|
480
541
|
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// When key-exchange channel opens, send our public key.
|
|
494
|
-
pc.router.on("channelOpen", ({ name, channel }: { name: string; channel: RTCDataChannel }) => {
|
|
495
|
-
if (name === KEY_EXCHANGE_CHANNEL) {
|
|
496
|
-
channel.send(e2ee.getKeyExchangeMessage());
|
|
542
|
+
// Drain buffered key-exchange channel open
|
|
543
|
+
if (pendingKeyExchangeChannel) {
|
|
544
|
+
pendingKeyExchangeChannel.send(e2ee.getKeyExchangeMessage());
|
|
545
|
+
}
|
|
546
|
+
// Drain buffered messages
|
|
547
|
+
for (const msg of pendingMessages) {
|
|
548
|
+
try {
|
|
549
|
+
await e2ee.handleKeyExchange(msg);
|
|
550
|
+
} catch (err) {
|
|
551
|
+
this.emit("e2eeFailed", { peerId, error: err });
|
|
497
552
|
}
|
|
498
|
-
}
|
|
553
|
+
}
|
|
499
554
|
|
|
500
555
|
e2ee.on("established", () => {
|
|
501
556
|
this.emit("e2eeEstablished", { peerId });
|
|
502
|
-
// Now that E2EE is ready, start Y.js sync (deferred).
|
|
503
557
|
this.startDataSync(peerId, pc);
|
|
504
558
|
});
|
|
505
559
|
|
|
506
560
|
e2ee.on("error", (err: Error) => {
|
|
507
561
|
this.emit("e2eeFailed", { peerId, error: err });
|
|
508
562
|
});
|
|
509
|
-
}
|
|
510
|
-
|
|
563
|
+
}).catch((err) => {
|
|
564
|
+
this.emit("e2eeFailed", { peerId, error: err });
|
|
511
565
|
this.startDataSync(peerId, pc);
|
|
512
|
-
}
|
|
566
|
+
});
|
|
513
567
|
}
|
|
514
568
|
|
|
515
569
|
private startDataSync(peerId: string, pc: PeerConnection): void {
|
package/src/webrtc/types.ts
CHANGED
|
@@ -158,7 +158,7 @@ export interface AbracadabraWebRTCConfiguration {
|
|
|
158
158
|
* When provided, all data channel messages (except key-exchange) are
|
|
159
159
|
* encrypted with AES-256-GCM using X25519 ECDH-derived session keys.
|
|
160
160
|
*/
|
|
161
|
-
e2ee?: import("./E2EEChannel.ts").E2EEIdentity;
|
|
161
|
+
e2ee?: import("./E2EEChannel.ts").E2EEIdentity | (() => Promise<import("./E2EEChannel.ts").E2EEIdentity>);
|
|
162
162
|
|
|
163
163
|
/** WebSocket polyfill for signaling (e.g. for Node.js). */
|
|
164
164
|
WebSocketPolyfill?: any;
|