@abraca/dabra 1.0.3 → 1.0.5

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.
@@ -1908,7 +1908,6 @@ var AbracadabraWS = class extends EventEmitter {
1908
1908
  if (this.connectionAttempt) this.rejectConnectionAttempt();
1909
1909
  this.status = WebSocketStatus.Disconnected;
1910
1910
  this.emit("status", { status: WebSocketStatus.Disconnected });
1911
- console.log("[DEBUG] onClose event:", typeof event, JSON.stringify(event), "code:", event?.code);
1912
1911
  const isRateLimited = event?.code === 4429 || event === 4429;
1913
1912
  this.emit("disconnect", { event });
1914
1913
  if (isRateLimited) this.emit("rateLimited");
@@ -2966,7 +2965,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2966
2965
  const childProvider = new AbracadabraProvider({
2967
2966
  name: childId,
2968
2967
  document: childDoc,
2969
- url: this.abracadabraConfig.url ?? this.configuration.websocketProvider?.url,
2968
+ websocketProvider: this.configuration.websocketProvider,
2970
2969
  token: this.configuration.token,
2971
2970
  subdocLoading: this.subdocLoading,
2972
2971
  disableOfflineStore: this.abracadabraConfig.disableOfflineStore,
@@ -2976,6 +2975,7 @@ var AbracadabraProvider = class AbracadabraProvider extends AbracadabraBaseProvi
2976
2975
  docKeyManager: this.abracadabraConfig.docKeyManager,
2977
2976
  keystore: this.abracadabraConfig.keystore
2978
2977
  });
2978
+ childProvider.attach();
2979
2979
  this.childProviders.set(childId, childProvider);
2980
2980
  this.emit("subdocLoaded", {
2981
2981
  childId,
@@ -6944,7 +6944,7 @@ function fromBase64url(b64) {
6944
6944
  const DB_NAME = "abracadabra:identity";
6945
6945
  const STORE_NAME = "identity";
6946
6946
  const RECORD_KEY = "current";
6947
- const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-identity-v1");
6947
+ const HKDF_INFO$2 = new TextEncoder().encode("abracadabra-identity-v1");
6948
6948
  function openDb$4() {
6949
6949
  return new Promise((resolve, reject) => {
6950
6950
  const req = indexedDB.open(DB_NAME, 1);
@@ -6977,7 +6977,7 @@ async function dbDelete(db) {
6977
6977
  });
6978
6978
  }
6979
6979
  async function deriveAesKey(prfOutput, salt) {
6980
- const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$1, 32);
6980
+ const keyBytes = hkdf(sha256, new Uint8Array(prfOutput), salt, HKDF_INFO$2, 32);
6981
6981
  return crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
6982
6982
  }
6983
6983
  var CryptoIdentityKeystore = class {
@@ -7694,7 +7694,7 @@ var FileBlobStore = class extends EventEmitter {
7694
7694
  * Manages AES-256-GCM document keys for CSE and E2E encrypted documents.
7695
7695
  * Keys are wrapped per-user using X25519 ECDH + HKDF-SHA256 + AES-256-GCM.
7696
7696
  */
7697
- const HKDF_INFO = new TextEncoder().encode("abracadabra-dockey-v1");
7697
+ const HKDF_INFO$1 = new TextEncoder().encode("abracadabra-dockey-v1");
7698
7698
  function fromBase64$1(b64) {
7699
7699
  return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
7700
7700
  }
@@ -7738,7 +7738,7 @@ var DocKeyManager = class {
7738
7738
  async wrapKeyForRecipient(docKey, recipientX25519PubKey, docId) {
7739
7739
  const ephemeralPriv = crypto.getRandomValues(new Uint8Array(32));
7740
7740
  const ephemeralPub = x25519.getPublicKey(ephemeralPriv);
7741
- const keyBytes = hkdf(sha256, x25519.getSharedSecret(ephemeralPriv, recipientX25519PubKey), new TextEncoder().encode(docId), HKDF_INFO, 32);
7741
+ const keyBytes = hkdf(sha256, x25519.getSharedSecret(ephemeralPriv, recipientX25519PubKey), new TextEncoder().encode(docId), HKDF_INFO$1, 32);
7742
7742
  const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt"]);
7743
7743
  const rawDocKey = await crypto.subtle.exportKey("raw", docKey);
7744
7744
  const nonce = crypto.getRandomValues(new Uint8Array(12));
@@ -7756,7 +7756,7 @@ var DocKeyManager = class {
7756
7756
  const ephemeralPub = wrapped.slice(0, 32);
7757
7757
  const nonce = wrapped.slice(32, 44);
7758
7758
  const ciphertext = wrapped.slice(44);
7759
- const keyBytes = hkdf(sha256, x25519.getSharedSecret(recipientX25519PrivKey, ephemeralPub), new TextEncoder().encode(docId), HKDF_INFO, 32);
7759
+ const keyBytes = hkdf(sha256, x25519.getSharedSecret(recipientX25519PrivKey, ephemeralPub), new TextEncoder().encode(docId), HKDF_INFO$1, 32);
7760
7760
  const wrapKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["decrypt"]);
7761
7761
  const rawDocKey = await crypto.subtle.decrypt({
7762
7762
  name: "AES-GCM",
@@ -8748,15 +8748,23 @@ const SHA256_BYTES = 32;
8748
8748
 
8749
8749
  //#endregion
8750
8750
  //#region packages/provider/src/webrtc/DataChannelRouter.ts
8751
+ /** Name of the data channel used for E2EE key exchange. */
8752
+ const KEY_EXCHANGE_CHANNEL = "key-exchange";
8751
8753
  var DataChannelRouter = class extends EventEmitter {
8752
8754
  constructor(connection) {
8753
8755
  super();
8754
8756
  this.connection = connection;
8755
8757
  this.channels = /* @__PURE__ */ new Map();
8758
+ this.encryptor = null;
8759
+ this.plaintextChannels = new Set([KEY_EXCHANGE_CHANNEL]);
8756
8760
  this.connection.ondatachannel = (event) => {
8757
8761
  this.registerChannel(event.channel);
8758
8762
  };
8759
8763
  }
8764
+ /** Attach an E2EE encryptor. All channels (except key-exchange) will be encrypted. */
8765
+ setEncryptor(encryptor) {
8766
+ this.encryptor = encryptor;
8767
+ }
8760
8768
  /** Create a named data channel (initiator side). */
8761
8769
  createChannel(name, options) {
8762
8770
  const channel = this.connection.createDataChannel(name, options);
@@ -8772,12 +8780,35 @@ var DataChannelRouter = class extends EventEmitter {
8772
8780
  });
8773
8781
  if (opts.enableFileTransfer) this.createChannel(CHANNEL_NAMES.FILE_TRANSFER, { ordered: true });
8774
8782
  }
8783
+ /**
8784
+ * Create namespaced channels for a child/subdocument.
8785
+ * Channel names are prefixed with `{childId}:` to avoid collisions.
8786
+ */
8787
+ createSubdocChannels(childId, opts) {
8788
+ if (opts.enableDocSync) this.createChannel(`${childId}:${CHANNEL_NAMES.YJS_SYNC}`, { ordered: true });
8789
+ if (opts.enableAwareness) this.createChannel(`${childId}:${CHANNEL_NAMES.AWARENESS}`, {
8790
+ ordered: false,
8791
+ maxRetransmits: 0
8792
+ });
8793
+ }
8775
8794
  getChannel(name) {
8776
8795
  return this.channels.get(name) ?? null;
8777
8796
  }
8778
8797
  isOpen(name) {
8779
8798
  return this.channels.get(name)?.readyState === "open";
8780
8799
  }
8800
+ /**
8801
+ * Send data on a named channel, encrypting if E2EE is active.
8802
+ * Falls back to plaintext if no encryptor is set or for exempt channels.
8803
+ */
8804
+ async send(name, data) {
8805
+ const channel = this.channels.get(name);
8806
+ if (!channel || channel.readyState !== "open") return;
8807
+ if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
8808
+ const encrypted = await this.encryptor.encrypt(data);
8809
+ channel.send(encrypted);
8810
+ } else channel.send(data);
8811
+ }
8781
8812
  registerChannel(channel) {
8782
8813
  channel.binaryType = "arraybuffer";
8783
8814
  this.channels.set(channel.label, channel);
@@ -8791,10 +8822,22 @@ var DataChannelRouter = class extends EventEmitter {
8791
8822
  this.emit("channelClose", { name: channel.label });
8792
8823
  this.channels.delete(channel.label);
8793
8824
  };
8794
- channel.onmessage = (event) => {
8825
+ channel.onmessage = async (event) => {
8826
+ const name = channel.label;
8827
+ let data = event.data;
8828
+ if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name) && (data instanceof ArrayBuffer || data instanceof Uint8Array)) try {
8829
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
8830
+ data = await this.encryptor.decrypt(buf);
8831
+ } catch (err) {
8832
+ this.emit("channelError", {
8833
+ name,
8834
+ error: err
8835
+ });
8836
+ return;
8837
+ }
8795
8838
  this.emit("channelMessage", {
8796
- name: channel.label,
8797
- data: event.data
8839
+ name,
8840
+ data
8798
8841
  });
8799
8842
  };
8800
8843
  channel.onerror = (event) => {
@@ -8922,7 +8965,13 @@ var PeerConnection = class extends EventEmitter {
8922
8965
  * prevent echo loops with the server-based provider.
8923
8966
  */
8924
8967
  var YjsDataChannel = class {
8925
- constructor(document, awareness, router) {
8968
+ /**
8969
+ * @param document - The Y.Doc to sync
8970
+ * @param awareness - Optional Awareness instance
8971
+ * @param router - DataChannelRouter for the peer connection
8972
+ * @param channelPrefix - Optional prefix for subdocument channels (e.g. `"{childId}:"`)
8973
+ */
8974
+ constructor(document, awareness, router, channelPrefix) {
8926
8975
  this.document = document;
8927
8976
  this.awareness = awareness;
8928
8977
  this.router = router;
@@ -8930,52 +8979,49 @@ var YjsDataChannel = class {
8930
8979
  this.awarenessUpdateHandler = null;
8931
8980
  this.channelOpenHandler = null;
8932
8981
  this.channelMessageHandler = null;
8982
+ const prefix = channelPrefix ?? "";
8983
+ this.syncChannelName = `${prefix}${CHANNEL_NAMES.YJS_SYNC}`;
8984
+ this.awarenessChannelName = `${prefix}${CHANNEL_NAMES.AWARENESS}`;
8933
8985
  }
8934
8986
  /** Start listening for Y.js updates and data channel messages. */
8935
8987
  attach() {
8936
8988
  this.docUpdateHandler = (update, origin) => {
8937
8989
  if (origin === this) return;
8938
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
8939
- if (!channel || channel.readyState !== "open") return;
8990
+ if (!this.router.isOpen(this.syncChannelName)) return;
8940
8991
  const encoder = createEncoder();
8941
8992
  writeVarUint(encoder, YJS_MSG.UPDATE);
8942
8993
  writeVarUint8Array(encoder, update);
8943
- channel.send(toUint8Array(encoder));
8994
+ this.router.send(this.syncChannelName, toUint8Array(encoder));
8944
8995
  };
8945
8996
  this.document.on("update", this.docUpdateHandler);
8946
8997
  if (this.awareness) {
8947
8998
  this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
8948
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8949
- if (!channel || channel.readyState !== "open") return;
8999
+ if (!this.router.isOpen(this.awarenessChannelName)) return;
8950
9000
  const changedClients = added.concat(updated).concat(removed);
8951
9001
  const update = encodeAwarenessUpdate(this.awareness, changedClients);
8952
- channel.send(update);
9002
+ this.router.send(this.awarenessChannelName, update);
8953
9003
  };
8954
9004
  this.awareness.on("update", this.awarenessUpdateHandler);
8955
9005
  }
8956
9006
  this.channelMessageHandler = ({ name, data }) => {
8957
- if (name === CHANNEL_NAMES.YJS_SYNC) this.handleSyncMessage(data);
8958
- else if (name === CHANNEL_NAMES.AWARENESS) this.handleAwarenessMessage(data);
9007
+ if (name === this.syncChannelName) this.handleSyncMessage(data);
9008
+ else if (name === this.awarenessChannelName) this.handleAwarenessMessage(data);
8959
9009
  };
8960
9010
  this.router.on("channelMessage", this.channelMessageHandler);
8961
9011
  this.channelOpenHandler = ({ name }) => {
8962
- if (name === CHANNEL_NAMES.YJS_SYNC) this.sendSyncStep1();
8963
- else if (name === CHANNEL_NAMES.AWARENESS && this.awareness) {
8964
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8965
- if (channel?.readyState === "open") {
9012
+ if (name === this.syncChannelName) this.sendSyncStep1();
9013
+ else if (name === this.awarenessChannelName && this.awareness) {
9014
+ if (this.router.isOpen(this.awarenessChannelName)) {
8966
9015
  const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8967
- channel.send(update);
9016
+ this.router.send(this.awarenessChannelName, update);
8968
9017
  }
8969
9018
  }
8970
9019
  };
8971
9020
  this.router.on("channelOpen", this.channelOpenHandler);
8972
- if (this.router.isOpen(CHANNEL_NAMES.YJS_SYNC)) this.sendSyncStep1();
8973
- if (this.awareness && this.router.isOpen(CHANNEL_NAMES.AWARENESS)) {
8974
- const channel = this.router.getChannel(CHANNEL_NAMES.AWARENESS);
8975
- if (channel?.readyState === "open") {
8976
- const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
8977
- channel.send(update);
8978
- }
9021
+ if (this.router.isOpen(this.syncChannelName)) this.sendSyncStep1();
9022
+ if (this.awareness && this.router.isOpen(this.awarenessChannelName)) {
9023
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
9024
+ this.router.send(this.awarenessChannelName, update);
8979
9025
  }
8980
9026
  }
8981
9027
  /** Stop listening and clean up handlers. */
@@ -9002,12 +9048,11 @@ var YjsDataChannel = class {
9002
9048
  this.detach();
9003
9049
  }
9004
9050
  sendSyncStep1() {
9005
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
9006
- if (!channel || channel.readyState !== "open") return;
9051
+ if (!this.router.isOpen(this.syncChannelName)) return;
9007
9052
  const encoder = createEncoder();
9008
9053
  writeVarUint(encoder, YJS_MSG.SYNC);
9009
9054
  writeSyncStep1(encoder, this.document);
9010
- channel.send(toUint8Array(encoder));
9055
+ this.router.send(this.syncChannelName, toUint8Array(encoder));
9011
9056
  }
9012
9057
  handleSyncMessage(data) {
9013
9058
  const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
@@ -9020,8 +9065,7 @@ var YjsDataChannel = class {
9020
9065
  const responseEncoder = createEncoder();
9021
9066
  writeVarUint(responseEncoder, YJS_MSG.SYNC);
9022
9067
  writeUint8Array(responseEncoder, toUint8Array(encoder));
9023
- const channel = this.router.getChannel(CHANNEL_NAMES.YJS_SYNC);
9024
- if (channel?.readyState === "open") channel.send(toUint8Array(responseEncoder));
9068
+ if (this.router.isOpen(this.syncChannelName)) this.router.send(this.syncChannelName, toUint8Array(responseEncoder));
9025
9069
  }
9026
9070
  if (syncMessageType === messageYjsSyncStep2) this.isSynced = true;
9027
9071
  } else if (msgType === YJS_MSG.UPDATE) {
@@ -9271,6 +9315,136 @@ function constantTimeEqual(a, b) {
9271
9315
  return result === 0;
9272
9316
  }
9273
9317
 
9318
+ //#endregion
9319
+ //#region packages/provider/src/webrtc/E2EEChannel.ts
9320
+ /**
9321
+ * E2EEChannel
9322
+ *
9323
+ * Per-peer end-to-end encryption for WebRTC data channels using
9324
+ * X25519 ECDH key agreement + HKDF-SHA256 + AES-256-GCM.
9325
+ *
9326
+ * Leverages the same cryptographic primitives as `DocKeyManager` and
9327
+ * `CryptoIdentityKeystore` but applied to data channel messages rather
9328
+ * than stored document keys.
9329
+ *
9330
+ * Key agreement flow:
9331
+ * 1. Both peers exchange Ed25519 public keys via the `key-exchange` data channel
9332
+ * 2. Convert Ed25519 → X25519 (Montgomery form)
9333
+ * 3. Run X25519 ECDH to derive a shared secret
9334
+ * 4. HKDF-SHA256(sharedSecret, salt, info) → 32-byte AES-256-GCM session key
9335
+ * 5. All subsequent data channel messages are encrypted with this key
9336
+ *
9337
+ * Message wire format: [12-byte nonce || AES-256-GCM ciphertext]
9338
+ *
9339
+ * Dependencies: @noble/curves (already in the project for CryptoIdentityKeystore)
9340
+ */
9341
+ const HKDF_INFO = new TextEncoder().encode("abracadabra-webrtc-e2ee-v1");
9342
+ const NONCE_BYTES = 12;
9343
+ var E2EEChannel = class extends EventEmitter {
9344
+ constructor(identity, docId) {
9345
+ super();
9346
+ this.identity = identity;
9347
+ this.docId = docId;
9348
+ this.sessionKey = null;
9349
+ this.remotePublicKey = null;
9350
+ this._isEstablished = false;
9351
+ }
9352
+ get isEstablished() {
9353
+ return this._isEstablished;
9354
+ }
9355
+ /**
9356
+ * Process a key-exchange message from the remote peer.
9357
+ * Called when the `key-exchange` data channel receives a message.
9358
+ *
9359
+ * The message is the remote peer's raw 32-byte Ed25519 public key.
9360
+ * After receiving it, ECDH is computed and the session key derived.
9361
+ */
9362
+ async handleKeyExchange(remoteEdPubKey) {
9363
+ if (remoteEdPubKey.length !== 32) {
9364
+ this.emit("error", /* @__PURE__ */ new Error("Invalid remote public key length"));
9365
+ return;
9366
+ }
9367
+ this.remotePublicKey = remoteEdPubKey;
9368
+ const remoteX25519Pub = ed25519.utils.toMontgomery(remoteEdPubKey);
9369
+ const sharedSecret = x25519.getSharedSecret(this.identity.x25519PrivateKey, remoteX25519Pub);
9370
+ const localPub = this.identity.publicKey;
9371
+ const remotePub = remoteEdPubKey;
9372
+ const [first, second] = this.sortKeys(localPub, remotePub);
9373
+ const saltParts = [
9374
+ new TextEncoder().encode(this.docId),
9375
+ first,
9376
+ second
9377
+ ];
9378
+ const salt = new Uint8Array(saltParts.reduce((acc, p) => acc + p.length, 0));
9379
+ let offset = 0;
9380
+ for (const part of saltParts) {
9381
+ salt.set(part, offset);
9382
+ offset += part.length;
9383
+ }
9384
+ const keyBytes = hkdf(sha256, sharedSecret, salt, HKDF_INFO, 32);
9385
+ this.sessionKey = await crypto.subtle.importKey("raw", keyBytes, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
9386
+ this._isEstablished = true;
9387
+ this.emit("established", { remotePublicKey: remoteEdPubKey });
9388
+ }
9389
+ /**
9390
+ * Returns the local Ed25519 public key to send to the remote peer
9391
+ * via the `key-exchange` data channel.
9392
+ */
9393
+ getKeyExchangeMessage() {
9394
+ return this.identity.publicKey;
9395
+ }
9396
+ /**
9397
+ * Encrypt a message for sending over a data channel.
9398
+ * Returns `[12-byte nonce || AES-256-GCM ciphertext]`.
9399
+ *
9400
+ * @throws if the session key has not been established yet.
9401
+ */
9402
+ async encrypt(plaintext) {
9403
+ if (!this.sessionKey) throw new Error("E2EE session key not established");
9404
+ const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
9405
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({
9406
+ name: "AES-GCM",
9407
+ iv: nonce
9408
+ }, this.sessionKey, plaintext));
9409
+ const result = new Uint8Array(NONCE_BYTES + ciphertext.length);
9410
+ result.set(nonce, 0);
9411
+ result.set(ciphertext, NONCE_BYTES);
9412
+ return result;
9413
+ }
9414
+ /**
9415
+ * Decrypt a message received from a data channel.
9416
+ * Expects `[12-byte nonce || AES-256-GCM ciphertext]`.
9417
+ *
9418
+ * @throws if the session key has not been established or decryption fails.
9419
+ */
9420
+ async decrypt(data) {
9421
+ if (!this.sessionKey) throw new Error("E2EE session key not established");
9422
+ if (data.length < NONCE_BYTES + 16) throw new Error("E2EE ciphertext too short");
9423
+ const nonce = data.slice(0, NONCE_BYTES);
9424
+ const ciphertext = data.slice(NONCE_BYTES);
9425
+ const plaintext = await crypto.subtle.decrypt({
9426
+ name: "AES-GCM",
9427
+ iv: nonce
9428
+ }, this.sessionKey, ciphertext);
9429
+ return new Uint8Array(plaintext);
9430
+ }
9431
+ /** Destroy the session key and wipe sensitive material. */
9432
+ destroy() {
9433
+ this.sessionKey = null;
9434
+ this.remotePublicKey = null;
9435
+ this._isEstablished = false;
9436
+ this.removeAllListeners();
9437
+ }
9438
+ /** Sort two keys lexicographically so both peers produce the same order. */
9439
+ sortKeys(a, b) {
9440
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
9441
+ if (a[i] < b[i]) return [a, b];
9442
+ if (a[i] > b[i]) return [b, a];
9443
+ }
9444
+ return a.length <= b.length ? [a, b] : [b, a];
9445
+ }
9446
+ };
9447
+
9274
9448
  //#endregion
9275
9449
  //#region packages/provider/src/webrtc/AbracadabraWebRTC.ts
9276
9450
  const HAS_RTC = typeof globalThis.RTCPeerConnection !== "undefined";
@@ -9291,6 +9465,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9291
9465
  this.peerConnections = /* @__PURE__ */ new Map();
9292
9466
  this.yjsChannels = /* @__PURE__ */ new Map();
9293
9467
  this.fileChannels = /* @__PURE__ */ new Map();
9468
+ this.e2eeChannels = /* @__PURE__ */ new Map();
9294
9469
  this.peers = /* @__PURE__ */ new Map();
9295
9470
  this.localPeerId = null;
9296
9471
  this.isConnected = false;
@@ -9299,6 +9474,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9299
9474
  this.config = {
9300
9475
  docId: configuration.docId,
9301
9476
  url: configuration.url,
9477
+ signalingUrl: configuration.signalingUrl ?? null,
9302
9478
  token: configuration.token,
9303
9479
  document: doc,
9304
9480
  awareness,
@@ -9309,6 +9485,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9309
9485
  enableAwarenessSync: configuration.enableAwarenessSync ?? !!awareness,
9310
9486
  enableFileTransfer: configuration.enableFileTransfer ?? false,
9311
9487
  fileChunkSize: configuration.fileChunkSize ?? 16384,
9488
+ e2ee: configuration.e2ee ?? null,
9312
9489
  WebSocketPolyfill: configuration.WebSocketPolyfill
9313
9490
  };
9314
9491
  if (configuration.autoConnect !== false && HAS_RTC) this.connect();
@@ -9489,6 +9666,11 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9489
9666
  }
9490
9667
  removePeer(peerId) {
9491
9668
  this.peers.delete(peerId);
9669
+ const e2ee = this.e2eeChannels.get(peerId);
9670
+ if (e2ee) {
9671
+ e2ee.destroy();
9672
+ this.e2eeChannels.delete(peerId);
9673
+ }
9492
9674
  const yjs = this.yjsChannels.get(peerId);
9493
9675
  if (yjs) {
9494
9676
  yjs.destroy();
@@ -9544,12 +9726,39 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9544
9726
  return pc;
9545
9727
  }
9546
9728
  attachDataHandlers(peerId, pc) {
9729
+ if (this.config.e2ee) {
9730
+ const e2ee = new E2EEChannel(this.config.e2ee, this.config.docId);
9731
+ this.e2eeChannels.set(peerId, e2ee);
9732
+ pc.router.setEncryptor(e2ee);
9733
+ pc.router.on("channelMessage", async ({ name, data }) => {
9734
+ if (name === KEY_EXCHANGE_CHANNEL) {
9735
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
9736
+ await e2ee.handleKeyExchange(buf);
9737
+ }
9738
+ });
9739
+ pc.router.on("channelOpen", ({ name, channel }) => {
9740
+ if (name === KEY_EXCHANGE_CHANNEL) channel.send(e2ee.getKeyExchangeMessage());
9741
+ });
9742
+ e2ee.on("established", () => {
9743
+ this.emit("e2eeEstablished", { peerId });
9744
+ this.startDataSync(peerId, pc);
9745
+ });
9746
+ e2ee.on("error", (err) => {
9747
+ this.emit("e2eeFailed", {
9748
+ peerId,
9749
+ error: err
9750
+ });
9751
+ });
9752
+ } else this.startDataSync(peerId, pc);
9753
+ }
9754
+ startDataSync(peerId, pc) {
9547
9755
  if (this.config.document && this.config.enableDocSync) {
9756
+ if (this.yjsChannels.has(peerId)) return;
9548
9757
  const yjs = new YjsDataChannel(this.config.document, this.config.enableAwarenessSync ? this.config.awareness : null, pc.router);
9549
9758
  yjs.attach();
9550
9759
  this.yjsChannels.set(peerId, yjs);
9551
9760
  }
9552
- if (this.config.enableFileTransfer) {
9761
+ if (this.config.enableFileTransfer && !this.fileChannels.has(peerId)) {
9553
9762
  const fc = new FileTransferChannel(pc.router, this.config.fileChunkSize);
9554
9763
  fc.on("receiveStart", (meta) => {
9555
9764
  this.emit("fileReceiveStart", {
@@ -9586,6 +9795,7 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9586
9795
  }
9587
9796
  async initiateConnection(peerId) {
9588
9797
  const pc = this.createPeerConnection(peerId);
9798
+ if (this.config.e2ee) pc.router.createChannel(KEY_EXCHANGE_CHANNEL, { ordered: true });
9589
9799
  pc.router.createDefaultChannels({
9590
9800
  enableDocSync: this.config.enableDocSync,
9591
9801
  enableAwareness: this.config.enableAwarenessSync,
@@ -9617,6 +9827,12 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9617
9827
  }
9618
9828
  }
9619
9829
  buildSignalingUrl() {
9830
+ if (this.config.signalingUrl) {
9831
+ let sigBase = this.config.signalingUrl;
9832
+ while (sigBase.endsWith("/")) sigBase = sigBase.slice(0, -1);
9833
+ sigBase = sigBase.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
9834
+ return `${sigBase}/ws/${encodeURIComponent(this.config.docId)}/signaling`;
9835
+ }
9620
9836
  let base = this.config.url;
9621
9837
  while (base.endsWith("/")) base = base.slice(0, -1);
9622
9838
  base = base.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
@@ -9624,6 +9840,310 @@ var AbracadabraWebRTC = class AbracadabraWebRTC extends EventEmitter {
9624
9840
  }
9625
9841
  };
9626
9842
 
9843
+ //#endregion
9844
+ //#region packages/provider/src/webrtc/ManualSignaling.ts
9845
+ /**
9846
+ * ManualSignaling
9847
+ *
9848
+ * Serverless signaling adapter for WebRTC peer-to-peer connections.
9849
+ * Instead of a WebSocket server relaying SDP/ICE, peers exchange
9850
+ * offer and answer "blobs" out-of-band (QR code, copy-paste, NFC, etc.).
9851
+ *
9852
+ * Designed as a drop-in replacement for `SignalingSocket` — emits the
9853
+ * same events (`welcome`, `joined`, `offer`, `answer`, `ice`) so
9854
+ * `AbracadabraWebRTC` can use it transparently.
9855
+ *
9856
+ * Flow:
9857
+ * Device A (initiator):
9858
+ * 1. `createOffer()` → gathers ICE candidates → returns offer blob
9859
+ * 2. Share blob via QR/paste
9860
+ * 3. Receive answer blob → `acceptAnswer(blob)`
9861
+ *
9862
+ * Device B (responder):
9863
+ * 1. Receive offer blob → `acceptOffer(blob)` → returns answer blob
9864
+ * 2. Share answer blob via QR/paste
9865
+ */
9866
+ var ManualSignaling = class extends EventEmitter {
9867
+ constructor(iceServers) {
9868
+ super();
9869
+ this.isConnected = false;
9870
+ this.pc = null;
9871
+ this.localPeerId = crypto.randomUUID();
9872
+ this.iceServers = iceServers ?? [{ urls: "stun:stun.l.google.com:19302" }];
9873
+ }
9874
+ /**
9875
+ * Initiator: create an offer blob with gathered ICE candidates.
9876
+ * Returns a blob to share with the remote peer.
9877
+ */
9878
+ async createOfferBlob() {
9879
+ this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
9880
+ const candidates = [];
9881
+ const gatheringComplete = new Promise((resolve) => {
9882
+ this.pc.onicecandidate = (event) => {
9883
+ if (event.candidate) candidates.push(JSON.stringify(event.candidate.toJSON()));
9884
+ else resolve();
9885
+ };
9886
+ });
9887
+ const offer = await this.pc.createOffer();
9888
+ await this.pc.setLocalDescription(offer);
9889
+ await gatheringComplete;
9890
+ this.isConnected = true;
9891
+ this.emit("welcome", {
9892
+ peerId: this.localPeerId,
9893
+ peers: []
9894
+ });
9895
+ return {
9896
+ sdp: this.pc.localDescription.sdp,
9897
+ candidates,
9898
+ peerId: this.localPeerId
9899
+ };
9900
+ }
9901
+ /**
9902
+ * Responder: accept an offer blob and create an answer blob.
9903
+ * The answer blob should be shared back to the initiator.
9904
+ */
9905
+ async acceptOffer(offerBlob) {
9906
+ this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
9907
+ const candidates = [];
9908
+ const gatheringComplete = new Promise((resolve) => {
9909
+ this.pc.onicecandidate = (event) => {
9910
+ if (event.candidate) candidates.push(JSON.stringify(event.candidate.toJSON()));
9911
+ else resolve();
9912
+ };
9913
+ });
9914
+ await this.pc.setRemoteDescription(new RTCSessionDescription({
9915
+ type: "offer",
9916
+ sdp: offerBlob.sdp
9917
+ }));
9918
+ for (const c of offerBlob.candidates) await this.pc.addIceCandidate(new RTCIceCandidate(JSON.parse(c)));
9919
+ const answer = await this.pc.createAnswer();
9920
+ await this.pc.setLocalDescription(answer);
9921
+ await gatheringComplete;
9922
+ this.isConnected = true;
9923
+ this.emit("welcome", {
9924
+ peerId: this.localPeerId,
9925
+ peers: []
9926
+ });
9927
+ this.emit("joined", {
9928
+ peer_id: offerBlob.peerId,
9929
+ user_id: offerBlob.peerId,
9930
+ muted: false,
9931
+ video: false,
9932
+ screen: false,
9933
+ name: null,
9934
+ color: null
9935
+ });
9936
+ this.emit("offer", {
9937
+ from: offerBlob.peerId,
9938
+ sdp: offerBlob.sdp
9939
+ });
9940
+ for (const c of offerBlob.candidates) this.emit("ice", {
9941
+ from: offerBlob.peerId,
9942
+ candidate: c
9943
+ });
9944
+ return {
9945
+ sdp: this.pc.localDescription.sdp,
9946
+ candidates,
9947
+ peerId: this.localPeerId
9948
+ };
9949
+ }
9950
+ /**
9951
+ * Initiator: accept the answer blob from the responder.
9952
+ * Completes the connection.
9953
+ */
9954
+ async acceptAnswer(answerBlob) {
9955
+ if (!this.pc) throw new Error("Call createOfferBlob() first");
9956
+ this.emit("joined", {
9957
+ peer_id: answerBlob.peerId,
9958
+ user_id: answerBlob.peerId,
9959
+ muted: false,
9960
+ video: false,
9961
+ screen: false,
9962
+ name: null,
9963
+ color: null
9964
+ });
9965
+ this.emit("answer", {
9966
+ from: answerBlob.peerId,
9967
+ sdp: answerBlob.sdp
9968
+ });
9969
+ for (const c of answerBlob.candidates) this.emit("ice", {
9970
+ from: answerBlob.peerId,
9971
+ candidate: c
9972
+ });
9973
+ }
9974
+ sendOffer(_to, _sdp) {}
9975
+ sendAnswer(_to, _sdp) {}
9976
+ sendIce(_to, _candidate) {}
9977
+ sendMute(_muted) {}
9978
+ sendMediaState(_video, _screen) {}
9979
+ sendProfile(_name, _color) {}
9980
+ sendLeave() {}
9981
+ async connect() {}
9982
+ disconnect() {
9983
+ this.isConnected = false;
9984
+ if (this.pc) {
9985
+ this.pc.close();
9986
+ this.pc = null;
9987
+ }
9988
+ this.emit("disconnected");
9989
+ }
9990
+ destroy() {
9991
+ this.disconnect();
9992
+ this.removeAllListeners();
9993
+ }
9994
+ };
9995
+
9996
+ //#endregion
9997
+ //#region packages/provider/src/sync/BroadcastChannelSync.ts
9998
+ /**
9999
+ * Cross-tab Y.js document and awareness sync via the BroadcastChannel API.
10000
+ *
10001
+ * Opens a BroadcastChannel per document, relays Y.js updates and awareness
10002
+ * state between tabs on the same origin. No server, no WebRTC — just same-device
10003
+ * multi-tab sync. Same-origin means same trust boundary, so no encryption needed.
10004
+ *
10005
+ * Uses the same y-protocols/sync encoding as `YjsDataChannel` for consistency.
10006
+ */
10007
+ const CHANNEL_PREFIX = "abra:sync:";
10008
+ /** Message type discriminators (first byte). */
10009
+ const MSG = {
10010
+ SYNC: 0,
10011
+ UPDATE: 1,
10012
+ AWARENESS: 2,
10013
+ QUERY_PEERS: 3
10014
+ };
10015
+ var BroadcastChannelSync = class BroadcastChannelSync extends EventEmitter {
10016
+ constructor(document, awareness, channelName) {
10017
+ super();
10018
+ this.document = document;
10019
+ this.awareness = awareness;
10020
+ this.channelName = channelName;
10021
+ this.channel = null;
10022
+ this.docUpdateHandler = null;
10023
+ this.awarenessUpdateHandler = null;
10024
+ this.destroyed = false;
10025
+ }
10026
+ /** Convenience factory using standard channel naming. */
10027
+ static forDoc(document, docId, awareness) {
10028
+ return new BroadcastChannelSync(document, awareness ?? null, `${CHANNEL_PREFIX}${docId}`);
10029
+ }
10030
+ /** Start syncing. Opens the BroadcastChannel and initiates a sync handshake. */
10031
+ connect() {
10032
+ if (this.destroyed || this.channel) return;
10033
+ if (typeof globalThis.BroadcastChannel === "undefined") return;
10034
+ this.channel = new BroadcastChannel(this.channelName);
10035
+ this.channel.onmessage = (event) => this.handleMessage(event.data);
10036
+ this.docUpdateHandler = (update, origin) => {
10037
+ if (origin === this) return;
10038
+ this.broadcastUpdate(update);
10039
+ };
10040
+ this.document.on("update", this.docUpdateHandler);
10041
+ if (this.awareness) {
10042
+ this.awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
10043
+ const changedClients = added.concat(updated).concat(removed);
10044
+ const update = encodeAwarenessUpdate(this.awareness, changedClients);
10045
+ this.broadcastAwareness(update);
10046
+ };
10047
+ this.awareness.on("update", this.awarenessUpdateHandler);
10048
+ }
10049
+ this.broadcastQueryPeers();
10050
+ this.sendSyncStep1();
10051
+ this.emit("connected");
10052
+ }
10053
+ /** Stop syncing and close the BroadcastChannel. */
10054
+ disconnect() {
10055
+ if (this.docUpdateHandler) {
10056
+ this.document.off("update", this.docUpdateHandler);
10057
+ this.docUpdateHandler = null;
10058
+ }
10059
+ if (this.awarenessUpdateHandler && this.awareness) {
10060
+ this.awareness.off("update", this.awarenessUpdateHandler);
10061
+ this.awarenessUpdateHandler = null;
10062
+ }
10063
+ if (this.channel) {
10064
+ this.channel.close();
10065
+ this.channel = null;
10066
+ }
10067
+ this.emit("disconnected");
10068
+ }
10069
+ /** Disconnect and prevent reconnection. */
10070
+ destroy() {
10071
+ this.disconnect();
10072
+ this.destroyed = true;
10073
+ this.removeAllListeners();
10074
+ }
10075
+ broadcastUpdate(update) {
10076
+ if (!this.channel) return;
10077
+ const encoder = createEncoder();
10078
+ writeVarUint(encoder, MSG.UPDATE);
10079
+ writeVarUint8Array(encoder, update);
10080
+ this.channel.postMessage(toUint8Array(encoder));
10081
+ }
10082
+ broadcastAwareness(update) {
10083
+ if (!this.channel) return;
10084
+ const encoder = createEncoder();
10085
+ writeVarUint(encoder, MSG.AWARENESS);
10086
+ writeVarUint8Array(encoder, update);
10087
+ this.channel.postMessage(toUint8Array(encoder));
10088
+ }
10089
+ sendSyncStep1() {
10090
+ if (!this.channel) return;
10091
+ const encoder = createEncoder();
10092
+ writeVarUint(encoder, MSG.SYNC);
10093
+ writeSyncStep1(encoder, this.document);
10094
+ this.channel.postMessage(toUint8Array(encoder));
10095
+ }
10096
+ broadcastQueryPeers() {
10097
+ if (!this.channel) return;
10098
+ const encoder = createEncoder();
10099
+ writeVarUint(encoder, MSG.QUERY_PEERS);
10100
+ this.channel.postMessage(toUint8Array(encoder));
10101
+ }
10102
+ handleMessage(data) {
10103
+ if (!(data instanceof ArrayBuffer || data instanceof Uint8Array)) return;
10104
+ const buf = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
10105
+ const decoder = createDecoder(buf);
10106
+ switch (readVarUint(decoder)) {
10107
+ case MSG.SYNC:
10108
+ this.handleSyncMessage(decoder);
10109
+ break;
10110
+ case MSG.UPDATE:
10111
+ this.handleUpdateMessage(decoder);
10112
+ break;
10113
+ case MSG.AWARENESS:
10114
+ this.handleAwarenessMessage(decoder);
10115
+ break;
10116
+ case MSG.QUERY_PEERS:
10117
+ this.sendSyncStep1();
10118
+ if (this.awareness) {
10119
+ const update = encodeAwarenessUpdate(this.awareness, Array.from(this.awareness.getStates().keys()));
10120
+ this.broadcastAwareness(update);
10121
+ }
10122
+ break;
10123
+ }
10124
+ }
10125
+ handleSyncMessage(decoder) {
10126
+ const encoder = createEncoder();
10127
+ const syncMessageType = readSyncMessage(decoder, encoder, this.document, this);
10128
+ if (length(encoder) > 0) {
10129
+ const responseEncoder = createEncoder();
10130
+ writeVarUint(responseEncoder, MSG.SYNC);
10131
+ writeUint8Array(responseEncoder, toUint8Array(encoder));
10132
+ this.channel?.postMessage(toUint8Array(responseEncoder));
10133
+ }
10134
+ if (syncMessageType === messageYjsSyncStep2) this.emit("synced");
10135
+ }
10136
+ handleUpdateMessage(decoder) {
10137
+ const update = readVarUint8Array(decoder);
10138
+ yjs.applyUpdate(this.document, update, this);
10139
+ }
10140
+ handleAwarenessMessage(decoder) {
10141
+ if (!this.awareness) return;
10142
+ const update = readVarUint8Array(decoder);
10143
+ applyAwarenessUpdate(this.awareness, update, this);
10144
+ }
10145
+ };
10146
+
9627
10147
  //#endregion
9628
10148
  exports.AbracadabraBaseProvider = AbracadabraBaseProvider;
9629
10149
  exports.AbracadabraClient = AbracadabraClient;
@@ -9634,6 +10154,7 @@ exports.AuthMessageType = AuthMessageType;
9634
10154
  exports.AwarenessError = AwarenessError;
9635
10155
  exports.BackgroundSyncManager = BackgroundSyncManager;
9636
10156
  exports.BackgroundSyncPersistence = BackgroundSyncPersistence;
10157
+ exports.BroadcastChannelSync = BroadcastChannelSync;
9637
10158
  exports.CHANNEL_NAMES = CHANNEL_NAMES;
9638
10159
  exports.ConnectionTimeout = ConnectionTimeout;
9639
10160
  exports.CryptoIdentityKeystore = CryptoIdentityKeystore;
@@ -9643,6 +10164,7 @@ exports.DataChannelRouter = DataChannelRouter;
9643
10164
  exports.DocKeyManager = DocKeyManager;
9644
10165
  exports.DocumentCache = DocumentCache;
9645
10166
  exports.E2EAbracadabraProvider = E2EAbracadabraProvider;
10167
+ exports.E2EEChannel = E2EEChannel;
9646
10168
  exports.E2EOfflineStore = E2EOfflineStore;
9647
10169
  exports.EncryptedYMap = EncryptedYMap;
9648
10170
  exports.EncryptedYText = EncryptedYText;
@@ -9652,6 +10174,8 @@ exports.FileTransferHandle = FileTransferHandle;
9652
10174
  exports.Forbidden = Forbidden;
9653
10175
  exports.HocuspocusProvider = HocuspocusProvider;
9654
10176
  exports.HocuspocusProviderWebsocket = HocuspocusProviderWebsocket;
10177
+ exports.KEY_EXCHANGE_CHANNEL = KEY_EXCHANGE_CHANNEL;
10178
+ exports.ManualSignaling = ManualSignaling;
9655
10179
  exports.MessageTooBig = MessageTooBig;
9656
10180
  exports.MessageType = MessageType;
9657
10181
  exports.OfflineStore = OfflineStore;