@abraca/dabra 1.2.0 → 1.3.0

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
@@ -584,6 +584,11 @@ declare class CryptoIdentityKeystore {
584
584
  * Returns the locally-cached username label, or null if no identity is cached.
585
585
  */
586
586
  getUsername(credentialIdHint?: string): Promise<string | null>;
587
+ /**
588
+ * Updates the cached username for a given credential (or the first cached identity).
589
+ * Call this after the user sets/changes their display name so it persists across devices.
590
+ */
591
+ setUsername(username: string, credentialIdHint?: string): Promise<void>;
587
592
  /** Returns true if an identity is cached in IndexedDB. */
588
593
  hasIdentity(): Promise<boolean>;
589
594
  /** Remove cached identity record(s) from IndexedDB. The passkey itself
@@ -2060,7 +2065,7 @@ interface AbracadabraWebRTCConfiguration {
2060
2065
  * When provided, all data channel messages (except key-exchange) are
2061
2066
  * encrypted with AES-256-GCM using X25519 ECDH-derived session keys.
2062
2067
  */
2063
- e2ee?: E2EEIdentity;
2068
+ e2ee?: E2EEIdentity | (() => Promise<E2EEIdentity>);
2064
2069
  /** WebSocket polyfill for signaling (e.g. for Node.js). */
2065
2070
  WebSocketPolyfill?: any;
2066
2071
  }
@@ -2121,6 +2126,9 @@ declare class AbracadabraWebRTC extends EventEmitter {
2121
2126
  private fileChannels;
2122
2127
  private e2eeChannels;
2123
2128
  private readonly config;
2129
+ /** Cached resolved E2EE identity (lazily resolved from factory on first peer connect). */
2130
+ private _resolvedE2ee;
2131
+ private _resolveE2eePromise;
2124
2132
  readonly peers: Map<string, PeerState>;
2125
2133
  localPeerId: string | null;
2126
2134
  isConnected: boolean;
@@ -2157,6 +2165,8 @@ declare class AbracadabraWebRTC extends EventEmitter {
2157
2165
  private removePeer;
2158
2166
  private removeAllPeers;
2159
2167
  private createPeerConnection;
2168
+ /** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
2169
+ private resolveE2ee;
2160
2170
  private attachDataHandlers;
2161
2171
  private startDataSync;
2162
2172
  private initiateConnection;
@@ -2530,8 +2540,8 @@ interface IdentityDocConfiguration {
2530
2540
  */
2531
2541
  webrtc?: {
2532
2542
  /** 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. */
2543
+ token: string | (() => string) | (() => Promise<string>); /** E2EE identity for the data channel. Accepts a factory for lazy derivation. */
2544
+ e2ee?: E2EEIdentity | (() => Promise<E2EEIdentity>); /** ICE servers. */
2535
2545
  iceServers?: RTCIceServer[];
2536
2546
  };
2537
2547
  /** 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.0",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -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;
@@ -471,40 +475,68 @@ export class AbracadabraWebRTC extends EventEmitter {
471
475
  return pc;
472
476
  }
473
477
 
478
+ /** Resolve the E2EE identity, supporting both pre-resolved objects and lazy factories. */
479
+ private async resolveE2ee(): Promise<E2EEIdentity | null> {
480
+ if (this._resolvedE2ee) return this._resolvedE2ee;
481
+ if (!this.config.e2ee) return null;
482
+ if (typeof this.config.e2ee === "function") {
483
+ if (!this._resolveE2eePromise) {
484
+ this._resolveE2eePromise = this.config.e2ee().then((id) => {
485
+ this._resolvedE2ee = id;
486
+ return id;
487
+ });
488
+ }
489
+ return this._resolveE2eePromise;
490
+ }
491
+ this._resolvedE2ee = this.config.e2ee;
492
+ return this._resolvedE2ee;
493
+ }
494
+
474
495
  private attachDataHandlers(peerId: string, pc: PeerConnection): void {
475
496
  // Set up E2EE if configured.
476
497
  if (this.config.e2ee) {
477
- const e2ee = new E2EEChannel(this.config.e2ee, this.config.docId);
478
- this.e2eeChannels.set(peerId, e2ee);
479
- pc.router.setEncryptor(e2ee);
480
-
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());
498
+ // Resolve E2EE identity (may be lazy — e.g. passkey-derived X25519 key).
499
+ this.resolveE2ee().then((identity) => {
500
+ if (!identity) {
501
+ this.startDataSync(peerId, pc);
502
+ return;
497
503
  }
498
- });
499
-
500
- e2ee.on("established", () => {
501
- this.emit("e2eeEstablished", { peerId });
502
- // Now that E2EE is ready, start Y.js sync (deferred).
503
- this.startDataSync(peerId, pc);
504
- });
504
+ const e2ee = new E2EEChannel(identity, this.config.docId);
505
+ this.e2eeChannels.set(peerId, e2ee);
506
+ pc.router.setEncryptor(e2ee);
507
+
508
+ // Listen for key-exchange messages on the router.
509
+ pc.router.on("channelMessage", async ({ name, data }: { name: string; data: any }) => {
510
+ if (name === KEY_EXCHANGE_CHANNEL) {
511
+ try {
512
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
513
+ await e2ee.handleKeyExchange(buf);
514
+ } catch (err) {
515
+ this.emit("e2eeFailed", { peerId, error: err });
516
+ }
517
+ }
518
+ });
505
519
 
506
- e2ee.on("error", (err: Error) => {
520
+ // When key-exchange channel opens, send our public key.
521
+ pc.router.on("channelOpen", ({ name, channel }: { name: string; channel: RTCDataChannel }) => {
522
+ if (name === KEY_EXCHANGE_CHANNEL) {
523
+ channel.send(e2ee.getKeyExchangeMessage());
524
+ }
525
+ });
526
+
527
+ e2ee.on("established", () => {
528
+ this.emit("e2eeEstablished", { peerId });
529
+ // Now that E2EE is ready, start Y.js sync (deferred).
530
+ this.startDataSync(peerId, pc);
531
+ });
532
+
533
+ e2ee.on("error", (err: Error) => {
534
+ this.emit("e2eeFailed", { peerId, error: err });
535
+ });
536
+ }).catch((err) => {
507
537
  this.emit("e2eeFailed", { peerId, error: err });
538
+ // Fall back to unencrypted sync on E2EE resolution failure.
539
+ this.startDataSync(peerId, pc);
508
540
  });
509
541
  } else {
510
542
  // No E2EE — start data sync immediately.
@@ -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;