@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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -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();
@@ -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
- // Set up E2EE if configured.
476
- if (this.config.e2ee) {
477
- const e2ee = new E2EEChannel(this.config.e2ee, this.config.docId);
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
- // Listen for key-exchange messages on the router.
482
- pc.router.on("channelMessage", async ({ name, data }: { name: string; data: any }) => {
483
- if (name === KEY_EXCHANGE_CHANNEL) {
484
- try {
485
- const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
486
- await e2ee.handleKeyExchange(buf);
487
- } catch (err) {
488
- this.emit("e2eeFailed", { peerId, error: err });
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
- } else {
510
- // No E2EE start data sync immediately.
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 {
@@ -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;