@abraca/dabra 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "1.3.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
  /**
@@ -288,6 +288,9 @@ export class AbracadabraWebRTC extends EventEmitter {
288
288
  this.signaling = null;
289
289
  }
290
290
 
291
+ this._resolvedE2ee = null;
292
+ this._resolveE2eePromise = null;
293
+
291
294
  this.removeAllListeners();
292
295
  }
293
296
 
@@ -493,55 +496,74 @@ export class AbracadabraWebRTC extends EventEmitter {
493
496
  }
494
497
 
495
498
  private attachDataHandlers(peerId: string, pc: PeerConnection): void {
496
- // Set up E2EE if configured.
497
- if (this.config.e2ee) {
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;
503
- }
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
- });
499
+ if (!this.config.e2ee) {
500
+ this.startDataSync(peerId, pc);
501
+ return;
502
+ }
519
503
 
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
- });
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
+ });
526
523
 
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
- });
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);
539
+ this.e2eeChannels.set(peerId, e2ee);
540
+ pc.router.setEncryptor(e2ee);
532
541
 
533
- e2ee.on("error", (err: Error) => {
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) {
534
551
  this.emit("e2eeFailed", { peerId, error: err });
535
- });
536
- }).catch((err) => {
537
- this.emit("e2eeFailed", { peerId, error: err });
538
- // Fall back to unencrypted sync on E2EE resolution failure.
552
+ }
553
+ }
554
+
555
+ e2ee.on("established", () => {
556
+ this.emit("e2eeEstablished", { peerId });
539
557
  this.startDataSync(peerId, pc);
540
558
  });
541
- } else {
542
- // No E2EE start data sync immediately.
559
+
560
+ e2ee.on("error", (err: Error) => {
561
+ this.emit("e2eeFailed", { peerId, error: err });
562
+ });
563
+ }).catch((err) => {
564
+ this.emit("e2eeFailed", { peerId, error: err });
543
565
  this.startDataSync(peerId, pc);
544
- }
566
+ });
545
567
  }
546
568
 
547
569
  private startDataSync(peerId: string, pc: PeerConnection): void {