@abraca/dabra 1.0.20 → 1.0.21

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.
@@ -3228,10 +3228,30 @@ var AbracadabraClient = class {
3228
3228
  async listKeys() {
3229
3229
  return (await this.request("GET", "/auth/keys")).keys;
3230
3230
  }
3231
+ /** Rename a registered device key. */
3232
+ async renameKey(keyId, deviceName) {
3233
+ await this.request("PATCH", `/auth/keys/${encodeURIComponent(keyId)}`, { body: { deviceName } });
3234
+ }
3231
3235
  /** Revoke a public key by its ID. */
3232
3236
  async revokeKey(keyId) {
3233
3237
  await this.request("DELETE", `/auth/keys/${encodeURIComponent(keyId)}`);
3234
3238
  }
3239
+ /** Create a single-use device invite code for pairing a new device to this account. */
3240
+ async createDeviceInvite(opts) {
3241
+ return this.request("POST", "/auth/device-invite", { body: opts?.expiresIn != null ? { expiresIn: opts.expiresIn } : {} });
3242
+ }
3243
+ /** Redeem a device invite code to register a new device key. Returns a JWT token. */
3244
+ async redeemDeviceInvite(opts) {
3245
+ return this.request("POST", "/auth/device-redeem", {
3246
+ body: {
3247
+ code: opts.code,
3248
+ publicKey: opts.publicKey,
3249
+ x25519Key: opts.x25519Key,
3250
+ deviceName: opts.deviceName
3251
+ },
3252
+ auth: false
3253
+ });
3254
+ }
3235
3255
  /** Get encryption info for a document. */
3236
3256
  async getDocEncryption(docId) {
3237
3257
  return this.request("GET", `/docs/${encodeURIComponent(docId)}/encryption`);
@@ -9949,19 +9969,23 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9949
9969
  }
9950
9970
  /**
9951
9971
  * Send a custom string message to a specific peer via a data channel.
9972
+ * When E2EE is active, the message is encrypted through the router.
9952
9973
  */
9953
9974
  sendCustomMessage(peerId, payload) {
9954
9975
  const pc = this.peerConnections.get(peerId);
9955
9976
  if (!pc) return;
9956
- let channel = pc.router.getChannel("custom");
9977
+ const channelName = "custom";
9978
+ let channel = pc.router.getChannel(channelName);
9957
9979
  if (!channel || channel.readyState !== "open") {
9958
- channel = pc.router.createChannel("custom", { ordered: true });
9980
+ channel = pc.router.createChannel(channelName, { ordered: true });
9959
9981
  channel.onopen = () => {
9960
- channel.send(payload);
9982
+ const data = new TextEncoder().encode(payload);
9983
+ pc.router.send(channelName, data);
9961
9984
  };
9962
9985
  return;
9963
9986
  }
9964
- channel.send(payload);
9987
+ const data = new TextEncoder().encode(payload);
9988
+ pc.router.send(channelName, data);
9965
9989
  }
9966
9990
  /**
9967
9991
  * Send a custom string message to all connected peers.
@@ -10308,6 +10332,270 @@ var ManualSignaling = class extends EventEmitter {
10308
10332
  }
10309
10333
  };
10310
10334
 
10335
+ //#endregion
10336
+ //#region packages/provider/src/webrtc/DevicePairingChannel.ts
10337
+ /**
10338
+ * DevicePairingChannel
10339
+ *
10340
+ * Enables cross-device identity pairing over an E2EE WebRTC data channel.
10341
+ * Device A (approver) creates a pairing session with a short code. Device B
10342
+ * (requester) joins with the code. After E2EE is established, Device B sends
10343
+ * its public key and Device A registers it via `addKey()`.
10344
+ *
10345
+ * Reuses the existing WebRTC stack: SignalingSocket, PeerConnection,
10346
+ * DataChannelRouter, and E2EEChannel (X25519 ECDH + AES-256-GCM).
10347
+ */
10348
+ /** Ambiguity-free charset (no 0/O/1/I). */
10349
+ const CODE_CHARSET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
10350
+ const CODE_LENGTH = 6;
10351
+ const PAIRING_TIMEOUT_MS = 300 * 1e3;
10352
+ function generatePairingCode() {
10353
+ const bytes = crypto.getRandomValues(new Uint8Array(CODE_LENGTH));
10354
+ return Array.from(bytes).map((b) => CODE_CHARSET[b % 32]).join("");
10355
+ }
10356
+ function codeToRoomId(code) {
10357
+ const hash = sha256(new TextEncoder().encode(code.toUpperCase()));
10358
+ return `__pairing_${Array.from(hash.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
10359
+ }
10360
+ var DevicePairingChannel = class DevicePairingChannel extends EventEmitter {
10361
+ constructor(role, pairingCode, config) {
10362
+ super();
10363
+ this.config = config;
10364
+ this.webrtc = null;
10365
+ this.timeoutHandle = null;
10366
+ this._destroyed = false;
10367
+ this._pendingRequest = null;
10368
+ this._connectedPeerId = null;
10369
+ this.role = role;
10370
+ this.pairingCode = pairingCode;
10371
+ }
10372
+ /**
10373
+ * Create an approver session (Device A). Returns the channel and a
10374
+ * 6-character pairing code to share with Device B.
10375
+ */
10376
+ static createApprover(config) {
10377
+ const code = generatePairingCode();
10378
+ const channel = new DevicePairingChannel("approver", code, config);
10379
+ channel.start();
10380
+ return {
10381
+ channel,
10382
+ pairingCode: code
10383
+ };
10384
+ }
10385
+ /**
10386
+ * Create a requester session (Device B). Joins with a pairing code
10387
+ * obtained from Device A.
10388
+ */
10389
+ static createRequester(config, pairingCode) {
10390
+ const channel = new DevicePairingChannel("requester", pairingCode.toUpperCase().replace(/[^A-Z2-9]/g, ""), config);
10391
+ channel.start();
10392
+ return channel;
10393
+ }
10394
+ /**
10395
+ * Approve the pending pairing request. Calls `client.addKey()` to
10396
+ * register Device B's public key, then notifies Device B.
10397
+ */
10398
+ async approve(client) {
10399
+ if (this.role !== "approver") return {
10400
+ success: false,
10401
+ error: "Only the approver can approve"
10402
+ };
10403
+ if (!this._pendingRequest) return {
10404
+ success: false,
10405
+ error: "No pending pairing request"
10406
+ };
10407
+ if (!this._connectedPeerId) return {
10408
+ success: false,
10409
+ error: "Peer not connected"
10410
+ };
10411
+ const req = this._pendingRequest;
10412
+ try {
10413
+ await client.addKey({
10414
+ publicKey: req.publicKey,
10415
+ deviceName: req.deviceName,
10416
+ x25519Key: req.x25519Key
10417
+ });
10418
+ this.sendMessage({ type: "pair-approved" });
10419
+ this._pendingRequest = null;
10420
+ this.emit("pairingComplete", { success: true });
10421
+ return { success: true };
10422
+ } catch (err) {
10423
+ const error = err?.message ?? "Failed to register device key";
10424
+ this.sendMessage({
10425
+ type: "pair-rejected",
10426
+ reason: error
10427
+ });
10428
+ this._pendingRequest = null;
10429
+ const result = {
10430
+ success: false,
10431
+ error
10432
+ };
10433
+ this.emit("pairingComplete", result);
10434
+ return result;
10435
+ }
10436
+ }
10437
+ /**
10438
+ * Approve via server-side device invite. Creates a single-use invite code
10439
+ * and sends it to Device B over the E2EE channel. Device B redeems it
10440
+ * independently via HTTP — Device A can go offline after this.
10441
+ */
10442
+ async approveWithInvite(client) {
10443
+ if (this.role !== "approver") return {
10444
+ success: false,
10445
+ error: "Only the approver can approve"
10446
+ };
10447
+ if (!this._pendingRequest) return {
10448
+ success: false,
10449
+ error: "No pending pairing request"
10450
+ };
10451
+ if (!this._connectedPeerId) return {
10452
+ success: false,
10453
+ error: "Peer not connected"
10454
+ };
10455
+ try {
10456
+ const { code } = await client.createDeviceInvite();
10457
+ this.sendMessage({
10458
+ type: "pair-invite-code",
10459
+ code
10460
+ });
10461
+ this._pendingRequest = null;
10462
+ this.emit("pairingComplete", { success: true });
10463
+ return { success: true };
10464
+ } catch (err) {
10465
+ const error = err?.message ?? "Failed to create device invite";
10466
+ this.sendMessage({
10467
+ type: "pair-rejected",
10468
+ reason: error
10469
+ });
10470
+ this._pendingRequest = null;
10471
+ const result = {
10472
+ success: false,
10473
+ error
10474
+ };
10475
+ this.emit("pairingComplete", result);
10476
+ return result;
10477
+ }
10478
+ }
10479
+ /**
10480
+ * Reject the pending pairing request.
10481
+ */
10482
+ reject(reason = "Rejected by user") {
10483
+ if (this.role !== "approver" || !this._pendingRequest) return;
10484
+ this.sendMessage({
10485
+ type: "pair-rejected",
10486
+ reason
10487
+ });
10488
+ this._pendingRequest = null;
10489
+ this.emit("pairingComplete", {
10490
+ success: false,
10491
+ error: reason
10492
+ });
10493
+ }
10494
+ /**
10495
+ * Send a pairing request to Device A. Call this after the "connected"
10496
+ * event fires.
10497
+ */
10498
+ requestPairing(request) {
10499
+ if (this.role !== "requester") return;
10500
+ this.sendMessage({
10501
+ type: "pair-request",
10502
+ publicKey: request.publicKey,
10503
+ x25519Key: request.x25519Key,
10504
+ deviceName: request.deviceName
10505
+ });
10506
+ }
10507
+ get isDestroyed() {
10508
+ return this._destroyed;
10509
+ }
10510
+ destroy() {
10511
+ if (this._destroyed) return;
10512
+ this._destroyed = true;
10513
+ if (this.timeoutHandle) {
10514
+ clearTimeout(this.timeoutHandle);
10515
+ this.timeoutHandle = null;
10516
+ }
10517
+ if (this.webrtc) {
10518
+ this.webrtc.destroy();
10519
+ this.webrtc = null;
10520
+ }
10521
+ this.removeAllListeners();
10522
+ }
10523
+ start() {
10524
+ this.webrtc = new AbracadabraWebRTC({
10525
+ docId: codeToRoomId(this.pairingCode),
10526
+ url: this.config.serverUrl,
10527
+ token: this.config.token,
10528
+ iceServers: this.config.iceServers,
10529
+ e2ee: this.config.e2ee,
10530
+ enableDocSync: false,
10531
+ enableAwarenessSync: false,
10532
+ enableFileTransfer: false,
10533
+ autoConnect: false,
10534
+ WebSocketPolyfill: this.config.WebSocketPolyfill
10535
+ });
10536
+ this.webrtc.on("e2eeEstablished", ({ peerId }) => {
10537
+ this._connectedPeerId = peerId;
10538
+ this.emit("connected");
10539
+ });
10540
+ this.webrtc.on("customMessage", ({ peerId, payload }) => {
10541
+ this.handleMessage(peerId, payload);
10542
+ });
10543
+ this.webrtc.on("peerLeft", () => {
10544
+ if (!this._destroyed) this.emit("error", /* @__PURE__ */ new Error("Peer disconnected"));
10545
+ });
10546
+ this.webrtc.on("signalingError", (err) => {
10547
+ this.emit("error", /* @__PURE__ */ new Error(`Signaling: ${err.message}`));
10548
+ });
10549
+ this.timeoutHandle = setTimeout(() => {
10550
+ if (!this._destroyed) {
10551
+ this.emit("error", /* @__PURE__ */ new Error("Pairing timed out"));
10552
+ this.destroy();
10553
+ }
10554
+ }, PAIRING_TIMEOUT_MS);
10555
+ this.webrtc.connect();
10556
+ }
10557
+ sendMessage(msg) {
10558
+ if (!this.webrtc || !this._connectedPeerId) return;
10559
+ this.webrtc.sendCustomMessage(this._connectedPeerId, JSON.stringify(msg));
10560
+ }
10561
+ handleMessage(peerId, payload) {
10562
+ let msg;
10563
+ try {
10564
+ msg = JSON.parse(payload);
10565
+ } catch {
10566
+ return;
10567
+ }
10568
+ switch (msg.type) {
10569
+ case "pair-request":
10570
+ if (this.role !== "approver") return;
10571
+ this._pendingRequest = {
10572
+ publicKey: msg.publicKey,
10573
+ x25519Key: msg.x25519Key,
10574
+ deviceName: msg.deviceName
10575
+ };
10576
+ this.emit("pairingRequest", this._pendingRequest);
10577
+ break;
10578
+ case "pair-approved":
10579
+ if (this.role !== "requester") return;
10580
+ this.emit("approved");
10581
+ this.emit("pairingComplete", { success: true });
10582
+ break;
10583
+ case "pair-rejected":
10584
+ if (this.role !== "requester") return;
10585
+ this.emit("rejected", msg.reason);
10586
+ this.emit("pairingComplete", {
10587
+ success: false,
10588
+ error: msg.reason
10589
+ });
10590
+ break;
10591
+ case "pair-invite-code":
10592
+ if (this.role !== "requester") return;
10593
+ this.emit("inviteCode", msg.code);
10594
+ break;
10595
+ }
10596
+ }
10597
+ };
10598
+
10311
10599
  //#endregion
10312
10600
  //#region packages/provider/src/sync/BroadcastChannelSync.ts
10313
10601
  /**
@@ -10476,6 +10764,7 @@ exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
10476
10764
  exports.DEFAULT_FILE_CHUNK_SIZE = DEFAULT_FILE_CHUNK_SIZE;
10477
10765
  exports.DEFAULT_ICE_SERVERS = DEFAULT_ICE_SERVERS;
10478
10766
  exports.DataChannelRouter = DataChannelRouter;
10767
+ exports.DevicePairingChannel = DevicePairingChannel;
10479
10768
  exports.DocKeyManager = DocKeyManager;
10480
10769
  exports.DocumentCache = DocumentCache;
10481
10770
  exports.E2EAbracadabraProvider = E2EAbracadabraProvider;